@kbediako/codex-orchestrator 0.1.38 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/README.md +46 -317
- package/bin/codex-orchestrator.js +161 -0
- package/codex.orchestrator.json +149 -13
- package/dist/bin/codex-orchestrator.js +797 -1154
- package/dist/orchestrator/src/cli/adapters/CommandBuilder.js +50 -0
- package/dist/orchestrator/src/cli/adapters/CommandPlanner.js +22 -4
- package/dist/orchestrator/src/cli/adapters/CommandReviewer.js +3 -3
- package/dist/orchestrator/src/cli/adapters/CommandTester.js +2 -2
- package/dist/orchestrator/src/cli/adapters/cloudFailureDiagnostics.js +295 -11
- package/dist/orchestrator/src/cli/coStatusAttachCliShell.js +402 -0
- package/dist/orchestrator/src/cli/coStatusCliShell.js +451 -0
- package/dist/orchestrator/src/cli/coStatusOperatorAutopilotCliShell.js +120 -0
- package/dist/orchestrator/src/cli/codexCliShell.js +119 -0
- package/dist/orchestrator/src/cli/codexDefaultsSetup.js +265 -36
- package/dist/orchestrator/src/cli/config/delegationConfig.js +317 -5
- package/dist/orchestrator/src/cli/config/repoConfigPolicy.js +2 -3
- package/dist/orchestrator/src/cli/config/userConfig.js +28 -13
- package/dist/orchestrator/src/cli/control/authenticatedControlRouteGate.js +69 -0
- package/dist/orchestrator/src/cli/control/authenticatedRouteComposition.js +267 -0
- package/dist/orchestrator/src/cli/control/authenticatedRouteController.js +5 -0
- package/dist/orchestrator/src/cli/control/authenticatedRouteDispatcher.js +41 -0
- package/dist/orchestrator/src/cli/control/compatibilityIssuePresenter.js +1035 -0
- package/dist/orchestrator/src/cli/control/confirmationApproveController.js +62 -0
- package/dist/orchestrator/src/cli/control/confirmationCreateController.js +69 -0
- package/dist/orchestrator/src/cli/control/confirmationIssueConsumeController.js +43 -0
- package/dist/orchestrator/src/cli/control/confirmationListController.js +22 -0
- package/dist/orchestrator/src/cli/control/confirmationValidateController.js +58 -0
- package/dist/orchestrator/src/cli/control/confirmations.js +25 -3
- package/dist/orchestrator/src/cli/control/controlActionCancelConfirmation.js +65 -0
- package/dist/orchestrator/src/cli/control/controlActionController.js +77 -0
- package/dist/orchestrator/src/cli/control/controlActionControllerSequencing.js +161 -0
- package/dist/orchestrator/src/cli/control/controlActionExecution.js +142 -0
- package/dist/orchestrator/src/cli/control/controlActionFinalization.js +43 -0
- package/dist/orchestrator/src/cli/control/controlActionOutcome.js +60 -0
- package/dist/orchestrator/src/cli/control/controlActionPreflight.js +476 -0
- package/dist/orchestrator/src/cli/control/controlAuthenticatedRouteHandoff.js +57 -0
- package/dist/orchestrator/src/cli/control/controlBootstrapAssembly.js +39 -0
- package/dist/orchestrator/src/cli/control/controlBootstrapMetadataPersistence.js +16 -0
- package/dist/orchestrator/src/cli/control/controlEventTransport.js +49 -0
- package/dist/orchestrator/src/cli/control/controlExpiryLifecycle.js +102 -0
- package/dist/orchestrator/src/cli/control/controlHostOwnership.js +480 -0
- package/dist/orchestrator/src/cli/control/controlHostSupervision.js +630 -0
- package/dist/orchestrator/src/cli/control/controlOversightFacade.js +8 -0
- package/dist/orchestrator/src/cli/control/controlOversightReadContract.js +1 -0
- package/dist/orchestrator/src/cli/control/controlOversightReadService.js +16 -0
- package/dist/orchestrator/src/cli/control/controlOversightUpdateContract.js +1 -0
- package/dist/orchestrator/src/cli/control/controlPersistenceFiles.js +6 -0
- package/dist/orchestrator/src/cli/control/controlQuestionChildResolution.js +18 -0
- package/dist/orchestrator/src/cli/control/controlRequestContext.js +42 -0
- package/dist/orchestrator/src/cli/control/controlRequestController.js +9 -0
- package/dist/orchestrator/src/cli/control/controlRequestPredispatch.js +17 -0
- package/dist/orchestrator/src/cli/control/controlRequestRouteDispatch.js +44 -0
- package/dist/orchestrator/src/cli/control/controlRuntime.js +1003 -0
- package/dist/orchestrator/src/cli/control/controlServer.js +23 -1456
- package/dist/orchestrator/src/cli/control/controlServerAuditAndErrorHelpers.js +115 -0
- package/dist/orchestrator/src/cli/control/controlServerAuthenticatedRouteBranch.js +29 -0
- package/dist/orchestrator/src/cli/control/controlServerBootstrapLifecycle.js +30 -0
- package/dist/orchestrator/src/cli/control/controlServerBootstrapStartSequence.js +21 -0
- package/dist/orchestrator/src/cli/control/controlServerOwnedRuntimeLifecycle.js +67 -0
- package/dist/orchestrator/src/cli/control/controlServerPublicLifecycle.js +756 -0
- package/dist/orchestrator/src/cli/control/controlServerPublicRouteHelpers.js +86 -0
- package/dist/orchestrator/src/cli/control/controlServerReadyInstanceLifecycle.js +25 -0
- package/dist/orchestrator/src/cli/control/controlServerReadyInstanceStartup.js +18 -0
- package/dist/orchestrator/src/cli/control/controlServerRequestBodyHelpers.js +37 -0
- package/dist/orchestrator/src/cli/control/controlServerRequestShell.js +40 -0
- package/dist/orchestrator/src/cli/control/controlServerRequestShellBinding.js +17 -0
- package/dist/orchestrator/src/cli/control/controlServerSeedLoading.js +27 -0
- package/dist/orchestrator/src/cli/control/controlServerSeededRuntimeAssembly.js +186 -0
- package/dist/orchestrator/src/cli/control/controlServerStartupInputPreparation.js +31 -0
- package/dist/orchestrator/src/cli/control/controlServerStartupSequence.js +49 -0
- package/dist/orchestrator/src/cli/control/controlState.js +233 -2
- package/dist/orchestrator/src/cli/control/controlStatusDashboard.js +1904 -0
- package/dist/orchestrator/src/cli/control/controlTelegramBridgeBootstrapLifecycle.js +22 -0
- package/dist/orchestrator/src/cli/control/controlTelegramBridgeLifecycle.js +67 -0
- package/dist/orchestrator/src/cli/control/controlTelegramBridgeOversightFacadeFactory.js +8 -0
- package/dist/orchestrator/src/cli/control/controlTelegramCommandController.js +49 -0
- package/dist/orchestrator/src/cli/control/controlTelegramDispatchRead.js +40 -0
- package/dist/orchestrator/src/cli/control/controlTelegramPollingController.js +89 -0
- package/dist/orchestrator/src/cli/control/controlTelegramProjectionNotificationController.js +29 -0
- package/dist/orchestrator/src/cli/control/controlTelegramPushState.js +63 -0
- package/dist/orchestrator/src/cli/control/controlTelegramQuestionRead.js +13 -0
- package/dist/orchestrator/src/cli/control/controlTelegramReadController.js +216 -0
- package/dist/orchestrator/src/cli/control/controlTelegramUpdateHandler.js +63 -0
- package/dist/orchestrator/src/cli/control/controlWatcher.js +73 -5
- package/dist/orchestrator/src/cli/control/delegationRegisterController.js +35 -0
- package/dist/orchestrator/src/cli/control/dynamicToolBridgePolicy.js +139 -0
- package/dist/orchestrator/src/cli/control/eventsSseController.js +12 -0
- package/dist/orchestrator/src/cli/control/linearBudgetState.js +1789 -0
- package/dist/orchestrator/src/cli/control/linearDispatchSource.js +1137 -0
- package/dist/orchestrator/src/cli/control/linearGraphqlClient.js +150 -0
- package/dist/orchestrator/src/cli/control/linearRateLimit.js +102 -0
- package/dist/orchestrator/src/cli/control/linearWebhookController.js +499 -0
- package/dist/orchestrator/src/cli/control/liveLinearAdvisoryRuntime.js +70 -0
- package/dist/orchestrator/src/cli/control/observabilityApiController.js +173 -0
- package/dist/orchestrator/src/cli/control/observabilityReadModel.js +500 -0
- package/dist/orchestrator/src/cli/control/observabilitySurface.js +284 -0
- package/dist/orchestrator/src/cli/control/observabilityUpdateNotifier.js +22 -0
- package/dist/orchestrator/src/cli/control/operatorDashboardPresenter.js +252 -0
- package/dist/orchestrator/src/cli/control/providerAgentCapacity.js +70 -0
- package/dist/orchestrator/src/cli/control/providerControlHostFreshnessGauge.js +1068 -0
- package/dist/orchestrator/src/cli/control/providerIntakeState.js +473 -0
- package/dist/orchestrator/src/cli/control/providerIssueHandoff.js +6811 -0
- package/dist/orchestrator/src/cli/control/providerIssueObservability.js +1348 -0
- package/dist/orchestrator/src/cli/control/providerIssueRetryQueue.js +84 -0
- package/dist/orchestrator/src/cli/control/providerLinearRuntimeProof.js +588 -0
- package/dist/orchestrator/src/cli/control/providerLinearScreenshotProof.js +473 -0
- package/dist/orchestrator/src/cli/control/providerLinearWorkerTruth.js +383 -0
- package/dist/orchestrator/src/cli/control/providerLinearWorkflowAudit.js +254 -0
- package/dist/orchestrator/src/cli/control/providerLinearWorkflowFacade.js +5573 -0
- package/dist/orchestrator/src/cli/control/providerLinearWorkflowStates.js +115 -0
- package/dist/orchestrator/src/cli/control/providerMergeCloseout.js +1868 -0
- package/dist/orchestrator/src/cli/control/providerOperatorAutopilot.js +1580 -0
- package/dist/orchestrator/src/cli/control/providerOperatorAutopilotLifecycle.js +154 -0
- package/dist/orchestrator/src/cli/control/providerOperatorAutopilotLocalRolloutExecution.js +1006 -0
- package/dist/orchestrator/src/cli/control/providerPollingHealth.js +435 -0
- package/dist/orchestrator/src/cli/control/providerTerminalCleanup.js +516 -0
- package/dist/orchestrator/src/cli/control/providerWorkerHosts.js +191 -0
- package/dist/orchestrator/src/cli/control/providerWorkflowConfigStore.js +515 -0
- package/dist/orchestrator/src/cli/control/questionChildResolutionAdapter.js +361 -0
- package/dist/orchestrator/src/cli/control/questionQueueController.js +181 -0
- package/dist/orchestrator/src/cli/control/questionReadRetryDeduplication.js +9 -0
- package/dist/orchestrator/src/cli/control/questionReadSequence.js +10 -0
- package/dist/orchestrator/src/cli/control/securityViolationController.js +27 -0
- package/dist/orchestrator/src/cli/control/selectedRunProjection.js +1885 -0
- package/dist/orchestrator/src/cli/control/telegramOversightApiClient.js +48 -0
- package/dist/orchestrator/src/cli/control/telegramOversightBridge.js +180 -0
- package/dist/orchestrator/src/cli/control/telegramOversightBridgeProjectionDeliveryQueue.js +25 -0
- package/dist/orchestrator/src/cli/control/telegramOversightBridgeRuntimeLifecycle.js +45 -0
- package/dist/orchestrator/src/cli/control/telegramOversightBridgeStateStore.js +77 -0
- package/dist/orchestrator/src/cli/control/telegramOversightControlActionApiClient.js +45 -0
- package/dist/orchestrator/src/cli/control/trackerDispatchPilot.js +439 -0
- package/dist/orchestrator/src/cli/control/uiDataController.js +34 -0
- package/dist/orchestrator/src/cli/control/uiSessionController.js +100 -0
- package/dist/orchestrator/src/cli/controlHostCliShell.js +860 -0
- package/dist/orchestrator/src/cli/controlHostFreshnessGaugeCliShell.js +129 -0
- package/dist/orchestrator/src/cli/controlHostSupervisionCliShell.js +2127 -0
- package/dist/orchestrator/src/cli/delegationCliShell.js +62 -0
- package/dist/orchestrator/src/cli/delegationServer.js +567 -678
- package/dist/orchestrator/src/cli/delegationServerCliShell.js +52 -0
- package/dist/orchestrator/src/cli/delegationServerQuestionFlowShell.js +228 -0
- package/dist/orchestrator/src/cli/delegationServerToolDispatchShell.js +411 -0
- package/dist/orchestrator/src/cli/delegationServerTransport.js +274 -0
- package/dist/orchestrator/src/cli/delegationSetup.js +51 -171
- package/dist/orchestrator/src/cli/devtoolsCliShell.js +34 -0
- package/dist/orchestrator/src/cli/doctor.js +678 -164
- package/dist/orchestrator/src/cli/doctorCliRequestShell.js +72 -0
- package/dist/orchestrator/src/cli/doctorCliShell.js +138 -0
- package/dist/orchestrator/src/cli/doctorUsage.js +119 -15
- package/dist/orchestrator/src/cli/exec/experience.js +16 -2
- package/dist/orchestrator/src/cli/exec/summary.js +3 -0
- package/dist/orchestrator/src/cli/execCliShell.js +51 -0
- package/dist/orchestrator/src/cli/flowCliRequestShell.js +44 -0
- package/dist/orchestrator/src/cli/flowCliShell.js +239 -0
- package/dist/orchestrator/src/cli/frontendTestCliRequestShell.js +80 -0
- package/dist/orchestrator/src/cli/frontendTestCliShell.js +41 -0
- package/dist/orchestrator/src/cli/init.js +95 -1
- package/dist/orchestrator/src/cli/initCliShell.js +50 -0
- package/dist/orchestrator/src/cli/linearCliShell.js +1200 -0
- package/dist/orchestrator/src/cli/mcpEnableCliShell.js +132 -0
- package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +3 -2
- package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +56 -0
- package/dist/orchestrator/src/cli/orchestrator.js +66 -1376
- package/dist/orchestrator/src/cli/planCliShell.js +19 -0
- package/dist/orchestrator/src/cli/prCliShell.js +41 -0
- package/dist/orchestrator/src/cli/providerLinearChildLanePhaseContract.js +204 -0
- package/dist/orchestrator/src/cli/providerLinearChildLaneRunner.js +1835 -0
- package/dist/orchestrator/src/cli/providerLinearChildLaneShell.js +2420 -0
- package/dist/orchestrator/src/cli/providerLinearChildStreamShell.js +385 -0
- package/dist/orchestrator/src/cli/providerLinearWorkerRunner.js +6834 -0
- package/dist/orchestrator/src/cli/resumeCliShell.js +14 -0
- package/dist/orchestrator/src/cli/reviewCliLaunchShell.js +72 -0
- package/dist/orchestrator/src/cli/rlm/alignment.js +3 -3
- package/dist/orchestrator/src/cli/rlm/context.js +94 -7
- package/dist/orchestrator/src/cli/rlm/rlmCodexRuntimeShell.js +546 -0
- package/dist/orchestrator/src/cli/rlm/symbolic.js +4 -2
- package/dist/orchestrator/src/cli/rlmCliRequestShell.js +42 -0
- package/dist/orchestrator/src/cli/rlmCompletionCliShell.js +46 -0
- package/dist/orchestrator/src/cli/rlmLaunchCliShell.js +51 -0
- package/dist/orchestrator/src/cli/rlmRunner.js +83 -523
- package/dist/orchestrator/src/cli/run/blockMemory.js +500 -0
- package/dist/orchestrator/src/cli/run/manifest.js +410 -73
- package/dist/orchestrator/src/cli/run/manifestPersister.js +45 -14
- package/dist/orchestrator/src/cli/run/runMemoryController.js +216 -0
- package/dist/orchestrator/src/cli/run/source0.js +690 -0
- package/dist/orchestrator/src/cli/run/workspacePath.js +101 -0
- package/dist/orchestrator/src/cli/runtime/mode.js +2 -1
- package/dist/orchestrator/src/cli/runtime/provider.js +39 -2
- package/dist/orchestrator/src/cli/selfCheckCliShell.js +12 -0
- package/dist/orchestrator/src/cli/services/commandRunner.js +698 -18
- package/dist/orchestrator/src/cli/services/execRuntime.js +66 -1
- package/dist/orchestrator/src/cli/services/orchestratorAutoScoutEvidenceRecorder.js +71 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudBranchResolution.js +8 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudEnvironmentResolution.js +22 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudExecutionLifecycleShell.js +39 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudPromptBuilder.js +37 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudRouteFallbackContract.js +45 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudRouteShell.js +36 -0
- package/dist/orchestrator/src/cli/services/orchestratorCloudTargetExecutor.js +277 -0
- package/dist/orchestrator/src/cli/services/orchestratorControlPlaneLifecycle.js +98 -0
- package/dist/orchestrator/src/cli/services/orchestratorControlPlaneLifecycleShell.js +54 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionLifecycle.js +112 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionModePolicy.js +27 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionRouteAdapterShell.js +59 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionRouteDecisionShell.js +57 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionRouteState.js +21 -0
- package/dist/orchestrator/src/cli/services/orchestratorExecutionRouter.js +2 -0
- package/dist/orchestrator/src/cli/services/orchestratorLocalPipelineExecutor.js +149 -0
- package/dist/orchestrator/src/cli/services/orchestratorLocalRouteShell.js +63 -0
- package/dist/orchestrator/src/cli/services/orchestratorPlanShell.js +54 -0
- package/dist/orchestrator/src/cli/services/orchestratorPlanTargetTracker.js +16 -0
- package/dist/orchestrator/src/cli/services/orchestratorResumePreparationShell.js +84 -0
- package/dist/orchestrator/src/cli/services/orchestratorResumeTokenValidation.js +15 -0
- package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleCompletion.js +31 -0
- package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleExecutionRegistration.js +37 -0
- package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleOrchestrationShell.js +83 -0
- package/dist/orchestrator/src/cli/services/orchestratorRunLifecycleTaskManagerShell.js +37 -0
- package/dist/orchestrator/src/cli/services/orchestratorRuntimeManifestMutation.js +20 -0
- package/dist/orchestrator/src/cli/services/orchestratorStartPreparationShell.js +56 -0
- package/dist/orchestrator/src/cli/services/orchestratorStatusShell.js +70 -0
- package/dist/orchestrator/src/cli/services/pipelineResolver.js +7 -3
- package/dist/orchestrator/src/cli/services/plannerMemory.js +119 -0
- package/dist/orchestrator/src/cli/services/runPreparation.js +7 -3
- package/dist/orchestrator/src/cli/services/runSummaryWriter.js +9 -0
- package/dist/orchestrator/src/cli/setupBootstrapShell.js +114 -0
- package/dist/orchestrator/src/cli/setupCliShell.js +51 -0
- package/dist/orchestrator/src/cli/skillsCliShell.js +56 -0
- package/dist/orchestrator/src/cli/startCliRequestShell.js +53 -0
- package/dist/orchestrator/src/cli/startCliShell.js +68 -0
- package/dist/orchestrator/src/cli/statusCliShell.js +22 -0
- package/dist/orchestrator/src/cli/utils/authProvenanceFingerprint.js +27 -0
- package/dist/orchestrator/src/cli/utils/cloudPreflight.js +285 -7
- package/dist/orchestrator/src/cli/utils/codexFeatures.js +60 -0
- package/dist/orchestrator/src/cli/utils/delegationConfigParser.js +250 -0
- package/dist/orchestrator/src/cli/utils/delegationMcpHealth.js +1382 -0
- package/dist/orchestrator/src/cli/utils/devtools.js +2 -54
- package/dist/orchestrator/src/cli/utils/mcpServerEntry.js +53 -0
- package/dist/orchestrator/src/cli/utils/packageProgramResolver.js +151 -0
- package/dist/orchestrator/src/cli/utils/providerOverrideEnv.js +71 -0
- package/dist/orchestrator/src/cli/utils/trailingJsonObject.js +59 -0
- package/dist/orchestrator/src/learning/crystalizer.js +2 -2
- package/dist/orchestrator/src/manager.js +74 -4
- package/dist/orchestrator/src/persistence/ExperienceStore.js +233 -49
- package/dist/orchestrator/src/persistence/TaskStateStore.js +6 -6
- package/dist/orchestrator/src/persistence/lockFile.js +70 -4
- package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +39 -0
- package/dist/orchestrator/src/sync/createCloudSyncWorker.js +3 -2
- package/dist/orchestrator/src/utils/atomicWrite.js +17 -2
- package/dist/packages/orchestrator/src/exec/unified-exec.js +99 -6
- package/dist/packages/orchestrator/src/instructions/promptPacks.js +150 -19
- package/dist/packages/sdk-node/src/orchestrator.js +137 -13
- package/dist/packages/shared/config/designConfig.js +8 -1
- package/dist/packages/shared/streams/stdio.js +1 -1
- package/dist/scripts/design/pipeline/permit.js +15 -0
- package/dist/scripts/lib/docs-catalog.js +399 -0
- package/dist/scripts/lib/docs-helpers.js +87 -5
- package/dist/scripts/lib/pr-watch-merge.js +1088 -80
- package/dist/scripts/lib/provider-run-contract.js +26 -0
- package/dist/scripts/lib/review-command-intent-classification.js +532 -0
- package/dist/scripts/lib/review-command-probe-classification.js +385 -0
- package/dist/scripts/lib/review-execution-boundary-preflight.js +279 -0
- package/dist/scripts/lib/review-execution-runtime.js +753 -0
- package/dist/scripts/lib/review-execution-state.js +1144 -0
- package/dist/scripts/lib/review-execution-telemetry.js +215 -0
- package/dist/scripts/lib/review-inspection-target-parsing.js +78 -0
- package/dist/scripts/lib/review-launch-attempt.js +601 -0
- package/dist/scripts/lib/review-meta-surface-boundary-analysis.js +300 -0
- package/dist/scripts/lib/review-meta-surface-normalization.js +746 -0
- package/dist/scripts/lib/review-non-interactive-handoff.js +61 -0
- package/dist/scripts/lib/review-prompt-context.js +376 -0
- package/dist/scripts/lib/review-scope-advisory.js +286 -0
- package/dist/scripts/lib/review-scope-paths.js +123 -0
- package/dist/scripts/lib/review-shell-command-parser.js +389 -0
- package/dist/scripts/lib/review-shell-env-interpreter.js +340 -0
- package/dist/scripts/lib/run-manifests.js +192 -36
- package/dist/scripts/lib/spark-policy-classifier.js +593 -0
- package/dist/scripts/run-review.js +507 -1777
- package/docs/README.md +43 -20
- package/docs/book/README.md +19 -0
- package/docs/book/codex-cli-0124-adoption.md +68 -0
- package/docs/book/local-hook-impact.md +73 -0
- package/docs/book/operations.md +60 -0
- package/docs/book/public-posture.md +34 -0
- package/docs/book/setup.md +91 -0
- package/docs/book/skills.md +11 -0
- package/docs/guides/codex-version-policy.md +104 -0
- package/docs/public/downstream-setup.md +113 -0
- package/docs/public/provider-onboarding.md +173 -0
- package/package.json +23 -10
- package/plugins/codex-orchestrator/.codex-plugin/plugin.json +30 -0
- package/plugins/codex-orchestrator/.mcp.json +13 -0
- package/plugins/codex-orchestrator/launcher.mjs +361 -0
- package/schemas/manifest.json +411 -0
- package/skills/README.md +26 -0
- package/skills/collab-subagents-first/SKILL.md +1 -1
- package/skills/delegation-usage/DELEGATION_GUIDE.md +30 -12
- package/skills/delegation-usage/SKILL.md +25 -14
- package/skills/land/SKILL.md +77 -0
- package/skills/linear/SKILL.md +255 -0
- package/skills/release/SKILL.md +47 -3
- package/skills/standalone-review/SKILL.md +6 -1
- package/templates/README.md +4 -2
- package/templates/codex/.codex/agents/awaiter-high.toml +2 -2
- package/templates/codex/.codex/agents/worker-complex.toml +1 -1
- package/templates/codex/.codex/config.toml +3 -4
- package/templates/codex/.codex/providers/README.md +13 -0
- package/templates/codex/.codex/providers/control.example.json +18 -0
- package/templates/codex/.codex/providers/provider.env.example +15 -0
- package/templates/codex/AGENTS.md +15 -8
- package/templates/codex/mcp-client.json +5 -1
- package/docs/assets/setup.gif +0 -0
|
@@ -0,0 +1,1789 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { acquireLockWithRetry } from '../../persistence/lockFile.js';
|
|
6
|
+
import { writeJsonAtomic } from '../utils/fs.js';
|
|
7
|
+
import { resolveCodexOrchestratorHome } from '../utils/codexPaths.js';
|
|
8
|
+
import { resolveLinearApiTokenFingerprint, resolveLinearRequestTimeoutMs } from './linearGraphqlClient.js';
|
|
9
|
+
import { extractLinearRateLimitDetailsFromHeaders } from './linearRateLimit.js';
|
|
10
|
+
const LINEAR_BUDGET_STATE_SCHEMA_VERSION = 2;
|
|
11
|
+
const LINEAR_BUDGET_ALIAS_SCHEMA_VERSION = 1;
|
|
12
|
+
const LINEAR_BUDGET_STATE_DIRNAME = 'linear-budget';
|
|
13
|
+
const LINEAR_BUDGET_SCOPE_DIRNAME = 'scopes';
|
|
14
|
+
const LINEAR_BUDGET_ALIAS_DIRNAME = 'aliases';
|
|
15
|
+
const LINEAR_BUDGET_DEFAULT_CONSTRAINED_POLL_INTERVAL_MS = 30_000;
|
|
16
|
+
const LINEAR_BUDGET_DEFAULT_LOW_POLL_INTERVAL_MS = 60_000;
|
|
17
|
+
const LINEAR_BUDGET_DEFAULT_ENDPOINT_CONSTRAINED_POLL_INTERVAL_MS = 45_000;
|
|
18
|
+
const LINEAR_BUDGET_DEFAULT_ENDPOINT_LOW_POLL_INTERVAL_MS = 90_000;
|
|
19
|
+
const LINEAR_BUDGET_REQUEST_HEADROOM_RESERVE_RATIO = 0.01;
|
|
20
|
+
const LINEAR_BUDGET_REQUEST_HEADROOM_RESERVE_MAX = 50;
|
|
21
|
+
const LINEAR_BUDGET_ENDPOINT_REQUEST_HEADROOM_RESERVE_RATIO = 0.05;
|
|
22
|
+
const LINEAR_BUDGET_ENDPOINT_REQUEST_HEADROOM_RESERVE_MAX = 5;
|
|
23
|
+
const LINEAR_BUDGET_REQUEST_BURN_HISTORY_LIMIT = 64;
|
|
24
|
+
const LINEAR_BUDGET_UNKNOWN_RESET_EXHAUSTED_GRACE_MS = LINEAR_BUDGET_DEFAULT_LOW_POLL_INTERVAL_MS;
|
|
25
|
+
const LINEAR_BUDGET_LOCK_RETRY = {
|
|
26
|
+
maxAttempts: 25,
|
|
27
|
+
initialDelayMs: 10,
|
|
28
|
+
backoffFactor: 1.5,
|
|
29
|
+
maxDelayMs: 250,
|
|
30
|
+
staleMs: 30_000
|
|
31
|
+
};
|
|
32
|
+
const LINEAR_BUDGET_RESERVATION_DEFAULT_TTL_GRACE_MS = 5_000;
|
|
33
|
+
const LINEAR_BUDGET_POLL_JITTER_RATIO = 0.1;
|
|
34
|
+
const LINEAR_BUDGET_POLL_JITTER_MAX_MS = 10_000;
|
|
35
|
+
export async function readSharedLinearBudgetStatus(env = process.env, options = {}) {
|
|
36
|
+
const paths = resolveLinearBudgetStatePaths(env);
|
|
37
|
+
if (!paths) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const persisted = await readNewestPersistedLinearBudgetStatus(paths, null);
|
|
41
|
+
return persisted ? hydrateLinearBudgetStatus(persisted, options) : null;
|
|
42
|
+
}
|
|
43
|
+
export async function recordLinearBudgetHeadersObservation(input) {
|
|
44
|
+
const details = extractLinearRateLimitDetailsFromHeaders(input.headers);
|
|
45
|
+
if (!hasRecordableLinearBudgetDetails(details)) {
|
|
46
|
+
return await readSharedLinearBudgetStatus(input.env ?? process.env);
|
|
47
|
+
}
|
|
48
|
+
return await recordLinearBudgetObservation({
|
|
49
|
+
env: input.env,
|
|
50
|
+
source: input.source,
|
|
51
|
+
details,
|
|
52
|
+
observedAt: input.observedAt,
|
|
53
|
+
scope: input.scope,
|
|
54
|
+
assumeUnknownResetsExhausted: false
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export async function recordLinearBudgetRateLimitObservation(input) {
|
|
58
|
+
if (!hasRecordableLinearBudgetDetails(input.rateLimit.details)) {
|
|
59
|
+
return await readSharedLinearBudgetStatus(input.env ?? process.env);
|
|
60
|
+
}
|
|
61
|
+
return await recordLinearBudgetObservation({
|
|
62
|
+
env: input.env,
|
|
63
|
+
source: input.source,
|
|
64
|
+
details: input.rateLimit.details,
|
|
65
|
+
observedAt: input.observedAt,
|
|
66
|
+
scope: input.scope,
|
|
67
|
+
assumeUnknownResetsExhausted: true
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export function resolveLinearBudgetPreflight(input) {
|
|
71
|
+
const budget = input.budget;
|
|
72
|
+
if (!budget) {
|
|
73
|
+
return { ok: true };
|
|
74
|
+
}
|
|
75
|
+
const minimumRequestsRemaining = normalizePositiveInteger(input.minimum_requests_remaining) ?? 1;
|
|
76
|
+
const inferredComplexityFloor = normalizePositiveInteger(input.minimum_complexity_remaining) ?? inferOperationComplexityFloor(budget.request_complexity, minimumRequestsRemaining);
|
|
77
|
+
const details = buildSharedLinearRateLimitDetails(budget, {
|
|
78
|
+
shared_budget_fail_fast: true,
|
|
79
|
+
operation: input.operation,
|
|
80
|
+
...(input.allow_below_request_reserve === true
|
|
81
|
+
? {
|
|
82
|
+
shared_budget_request_headroom_override: 'allow_below_request_reserve'
|
|
83
|
+
}
|
|
84
|
+
: {})
|
|
85
|
+
});
|
|
86
|
+
if (budget.cooldown_active) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: {
|
|
90
|
+
code: 'linear_rate_limited',
|
|
91
|
+
message: 'Linear shared budget cooldown is active.',
|
|
92
|
+
status: 429,
|
|
93
|
+
retryable: true,
|
|
94
|
+
details
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const requestShortfall = resolveBucketShortfall([
|
|
99
|
+
['requests', budget.requests],
|
|
100
|
+
['endpoint_requests', budget.endpoint_requests]
|
|
101
|
+
], minimumRequestsRemaining);
|
|
102
|
+
if (requestShortfall) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
error: {
|
|
106
|
+
code: 'linear_rate_limited',
|
|
107
|
+
message: `Linear shared budget is insufficient for ${input.operation}.`,
|
|
108
|
+
status: 429,
|
|
109
|
+
retryable: true,
|
|
110
|
+
details: {
|
|
111
|
+
...details,
|
|
112
|
+
required_requests_remaining: minimumRequestsRemaining,
|
|
113
|
+
shortfall_bucket: requestShortfall.bucket,
|
|
114
|
+
shortfall_remaining: requestShortfall.remaining
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (inferredComplexityFloor !== null) {
|
|
120
|
+
const complexityShortfall = resolveBucketShortfall([
|
|
121
|
+
['complexity', budget.complexity],
|
|
122
|
+
['endpoint_complexity', budget.endpoint_complexity]
|
|
123
|
+
], inferredComplexityFloor);
|
|
124
|
+
if (complexityShortfall) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
error: {
|
|
128
|
+
code: 'linear_rate_limited',
|
|
129
|
+
message: `Linear shared complexity budget is insufficient for ${input.operation}.`,
|
|
130
|
+
status: 429,
|
|
131
|
+
retryable: true,
|
|
132
|
+
details: {
|
|
133
|
+
...details,
|
|
134
|
+
required_complexity_remaining: inferredComplexityFloor,
|
|
135
|
+
shortfall_bucket: complexityShortfall.bucket,
|
|
136
|
+
shortfall_remaining: complexityShortfall.remaining
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (input.allow_below_request_reserve !== true) {
|
|
143
|
+
const requestReserveShortfall = resolveRequestReserveShortfall([
|
|
144
|
+
['requests', budget.requests],
|
|
145
|
+
['endpoint_requests', budget.endpoint_requests]
|
|
146
|
+
], minimumRequestsRemaining);
|
|
147
|
+
if (requestReserveShortfall) {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
error: {
|
|
151
|
+
code: 'linear_rate_limited',
|
|
152
|
+
message: `Linear shared request headroom reserve is insufficient for ${input.operation}.`,
|
|
153
|
+
status: 429,
|
|
154
|
+
retryable: true,
|
|
155
|
+
details: {
|
|
156
|
+
...details,
|
|
157
|
+
required_requests_remaining: minimumRequestsRemaining,
|
|
158
|
+
request_headroom_reserve_bucket: requestReserveShortfall.bucket,
|
|
159
|
+
request_headroom_remaining: requestReserveShortfall.remaining,
|
|
160
|
+
request_headroom_reserve: requestReserveShortfall.reserve,
|
|
161
|
+
request_headroom_usable_remaining: Math.max(0, requestReserveShortfall.usable_remaining)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const exhaustedBucket = findExhaustedLinearBudgetBucket(budget);
|
|
168
|
+
if (exhaustedBucket) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
error: {
|
|
172
|
+
code: 'linear_rate_limited',
|
|
173
|
+
message: 'Linear shared budget is exhausted.',
|
|
174
|
+
status: 429,
|
|
175
|
+
retryable: true,
|
|
176
|
+
details: {
|
|
177
|
+
...details,
|
|
178
|
+
exhausted_bucket: exhaustedBucket
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return { ok: true };
|
|
184
|
+
}
|
|
185
|
+
export async function reserveLinearBudgetReservation(input) {
|
|
186
|
+
const env = input.env ?? process.env;
|
|
187
|
+
const paths = resolveLinearBudgetStatePaths(env);
|
|
188
|
+
if (!paths) {
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
budget: null,
|
|
192
|
+
reservation: null
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const requestUnits = normalizePositiveInteger(input.request_units) ?? 1;
|
|
196
|
+
const reservationTtlMs = normalizePositiveInteger(input.ttl_ms) ??
|
|
197
|
+
resolveLinearRequestTimeoutMs(env) + LINEAR_BUDGET_RESERVATION_DEFAULT_TTL_GRACE_MS;
|
|
198
|
+
const lockScopeKey = await resolveLinearBudgetLockScopeKey(paths);
|
|
199
|
+
return await withLinearBudgetStateLock(paths, async () => {
|
|
200
|
+
const persisted = await readNewestPersistedLinearBudgetStatus(paths, null);
|
|
201
|
+
if (!persisted) {
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
budget: null,
|
|
205
|
+
reservation: null
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const cleanedPersisted = pruneExpiredReservations(persisted);
|
|
209
|
+
const selectedBudget = hydrateLinearBudgetStatus(cleanedPersisted, {
|
|
210
|
+
operation: input.operation
|
|
211
|
+
});
|
|
212
|
+
const inferredComplexityFloor = normalizePositiveInteger(input.minimum_complexity_remaining) ??
|
|
213
|
+
inferOperationComplexityFloor(selectedBudget.request_complexity, requestUnits);
|
|
214
|
+
const preflight = resolveLinearBudgetPreflight({
|
|
215
|
+
budget: selectedBudget,
|
|
216
|
+
operation: input.operation,
|
|
217
|
+
minimum_requests_remaining: normalizePositiveInteger(input.minimum_requests_remaining) ?? requestUnits,
|
|
218
|
+
minimum_complexity_remaining: inferredComplexityFloor,
|
|
219
|
+
allow_below_request_reserve: input.allow_below_request_reserve === true
|
|
220
|
+
});
|
|
221
|
+
if (!preflight.ok) {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
error: preflight.error
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const shouldReserve = selectedBudget.requests !== null ||
|
|
228
|
+
selectedBudget.complexity !== null ||
|
|
229
|
+
selectedBudget.endpoint_requests !== null ||
|
|
230
|
+
selectedBudget.endpoint_complexity !== null;
|
|
231
|
+
if (!shouldReserve) {
|
|
232
|
+
if (cleanedPersisted !== persisted) {
|
|
233
|
+
await writePersistedLinearBudgetStatus(paths, cleanedPersisted);
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
ok: true,
|
|
237
|
+
budget: selectedBudget,
|
|
238
|
+
reservation: null
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const expiresAt = new Date(Date.now() + reservationTtlMs).toISOString();
|
|
242
|
+
const reservationRecord = {
|
|
243
|
+
id: randomUUID(),
|
|
244
|
+
operation: input.operation,
|
|
245
|
+
endpoint_key: selectedBudget.selected_endpoint_key,
|
|
246
|
+
requests: requestUnits,
|
|
247
|
+
complexity: inferredComplexityFloor,
|
|
248
|
+
created_at: new Date().toISOString(),
|
|
249
|
+
expires_at: expiresAt
|
|
250
|
+
};
|
|
251
|
+
const nextPersisted = {
|
|
252
|
+
...cleanedPersisted,
|
|
253
|
+
reservations: [...cleanedPersisted.reservations, reservationRecord]
|
|
254
|
+
};
|
|
255
|
+
await writePersistedLinearBudgetStatus(paths, nextPersisted);
|
|
256
|
+
const hydrated = hydrateLinearBudgetStatus(nextPersisted, {
|
|
257
|
+
operation: input.operation
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
ok: true,
|
|
261
|
+
budget: hydrated,
|
|
262
|
+
reservation: {
|
|
263
|
+
id: reservationRecord.id,
|
|
264
|
+
endpoint_key: reservationRecord.endpoint_key,
|
|
265
|
+
endpoint_name: hydrated.endpoint_name,
|
|
266
|
+
requests: reservationRecord.requests,
|
|
267
|
+
complexity: reservationRecord.complexity,
|
|
268
|
+
release: async () => {
|
|
269
|
+
await releaseLinearBudgetReservation({
|
|
270
|
+
env,
|
|
271
|
+
reservationId: reservationRecord.id
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}, lockScopeKey);
|
|
277
|
+
}
|
|
278
|
+
export async function releaseLinearBudgetReservation(input) {
|
|
279
|
+
const reservationId = normalizeOptionalString(input.reservationId);
|
|
280
|
+
if (!reservationId) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const env = input.env ?? process.env;
|
|
284
|
+
const paths = resolveLinearBudgetStatePaths(env);
|
|
285
|
+
if (!paths) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const lockScopeKey = await resolveLinearBudgetLockScopeKey(paths);
|
|
289
|
+
await withLinearBudgetStateLock(paths, async () => {
|
|
290
|
+
const persisted = await readNewestPersistedLinearBudgetStatus(paths, null);
|
|
291
|
+
if (!persisted) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const nextReservations = persisted.reservations.filter((entry) => entry.id !== reservationId);
|
|
295
|
+
if (nextReservations.length === persisted.reservations.length) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
await writePersistedLinearBudgetStatus(paths, {
|
|
299
|
+
...persisted,
|
|
300
|
+
reservations: nextReservations
|
|
301
|
+
});
|
|
302
|
+
}, lockScopeKey);
|
|
303
|
+
}
|
|
304
|
+
export function resolveLinearPollingInterval(input) {
|
|
305
|
+
const budget = input.budget;
|
|
306
|
+
if (!budget) {
|
|
307
|
+
return {
|
|
308
|
+
interval_ms: input.default_interval_ms,
|
|
309
|
+
reason: null,
|
|
310
|
+
linear_budget: null
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
314
|
+
if (budget.cooldown_active) {
|
|
315
|
+
const cooldownUntilMs = parseIsoToMs(budget.cooldown_until);
|
|
316
|
+
const cooldownWaitMs = cooldownUntilMs === null ? input.default_interval_ms : Math.max(0, cooldownUntilMs - nowMs);
|
|
317
|
+
return {
|
|
318
|
+
interval_ms: Math.max(input.default_interval_ms, cooldownWaitMs),
|
|
319
|
+
reason: budget.suppression_reason ?? 'linear_budget_shared_cooldown',
|
|
320
|
+
linear_budget: budget
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const pressure = resolveMaterializedBudgetPressure(budget);
|
|
324
|
+
const requestHeadroomGuard = resolveRequestPollingHeadroomGuard({
|
|
325
|
+
budget,
|
|
326
|
+
defaultIntervalMs: input.default_interval_ms,
|
|
327
|
+
nowMs
|
|
328
|
+
});
|
|
329
|
+
if (pressure.suppression === 'none') {
|
|
330
|
+
if (requestHeadroomGuard) {
|
|
331
|
+
const intervalMs = applyDeterministicPositiveJitter(requestHeadroomGuard.interval_ms, `${requestHeadroomGuard.reason}|${budget.selected_endpoint_key ?? 'global'}|${budget.observed_at}`, nowMs);
|
|
332
|
+
return {
|
|
333
|
+
interval_ms: intervalMs,
|
|
334
|
+
reason: requestHeadroomGuard.reason,
|
|
335
|
+
linear_budget: {
|
|
336
|
+
...budget,
|
|
337
|
+
suppression: requestHeadroomGuard.suppression,
|
|
338
|
+
suppression_reason: requestHeadroomGuard.reason
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
interval_ms: input.default_interval_ms,
|
|
344
|
+
reason: null,
|
|
345
|
+
linear_budget: budget
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
let baseIntervalMs = input.default_interval_ms;
|
|
349
|
+
if (pressure.suppression === 'constrained') {
|
|
350
|
+
baseIntervalMs = Math.max(input.default_interval_ms, pressure.endpoint_specific
|
|
351
|
+
? LINEAR_BUDGET_DEFAULT_ENDPOINT_CONSTRAINED_POLL_INTERVAL_MS
|
|
352
|
+
: LINEAR_BUDGET_DEFAULT_CONSTRAINED_POLL_INTERVAL_MS);
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
baseIntervalMs = Math.max(input.default_interval_ms, pressure.endpoint_specific
|
|
356
|
+
? LINEAR_BUDGET_DEFAULT_ENDPOINT_LOW_POLL_INTERVAL_MS
|
|
357
|
+
: LINEAR_BUDGET_DEFAULT_LOW_POLL_INTERVAL_MS);
|
|
358
|
+
}
|
|
359
|
+
let reason = pressure.reason;
|
|
360
|
+
let suppression = pressure.suppression;
|
|
361
|
+
if (requestHeadroomGuard && requestHeadroomGuard.interval_ms > baseIntervalMs) {
|
|
362
|
+
baseIntervalMs = requestHeadroomGuard.interval_ms;
|
|
363
|
+
if (reason === null ||
|
|
364
|
+
reason.startsWith('linear_budget_requests_') ||
|
|
365
|
+
reason.startsWith('linear_budget_endpoint_requests_')) {
|
|
366
|
+
reason = requestHeadroomGuard.reason;
|
|
367
|
+
suppression = requestHeadroomGuard.suppression;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const intervalMs = applyDeterministicPositiveJitter(baseIntervalMs, `${reason ?? 'linear_budget'}|${budget.selected_endpoint_key ?? 'global'}|${budget.observed_at}`, nowMs);
|
|
371
|
+
return {
|
|
372
|
+
interval_ms: intervalMs,
|
|
373
|
+
reason,
|
|
374
|
+
linear_budget: {
|
|
375
|
+
...budget,
|
|
376
|
+
suppression,
|
|
377
|
+
suppression_reason: reason
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
export function buildSharedLinearRateLimitDetails(budget, extraDetails = {}) {
|
|
382
|
+
return {
|
|
383
|
+
...(budget.retry_after_seconds !== null ? { retry_after_seconds: budget.retry_after_seconds } : {}),
|
|
384
|
+
...(budget.request_id ? { request_id: budget.request_id } : {}),
|
|
385
|
+
...(budget.cooldown_until ? { shared_budget_cooldown_until: budget.cooldown_until } : {}),
|
|
386
|
+
shared_budget_observed_at: budget.observed_at,
|
|
387
|
+
shared_budget_source: budget.source,
|
|
388
|
+
shared_budget_cooldown_active: budget.cooldown_active,
|
|
389
|
+
shared_budget_suppression: budget.suppression,
|
|
390
|
+
shared_budget_scope_kind: budget.scope_kind,
|
|
391
|
+
shared_budget_scope_key: budget.scope_key,
|
|
392
|
+
...(budget.viewer_id ? { shared_budget_viewer_id: budget.viewer_id } : {}),
|
|
393
|
+
...(budget.workspace_id ? { shared_budget_workspace_id: budget.workspace_id } : {}),
|
|
394
|
+
...(budget.suppression_reason ? { shared_budget_suppression_reason: budget.suppression_reason } : {}),
|
|
395
|
+
...(budget.endpoint_name ? { endpoint_name: budget.endpoint_name } : {}),
|
|
396
|
+
...(budget.selected_endpoint_key ? { selected_endpoint_key: budget.selected_endpoint_key } : {}),
|
|
397
|
+
...(budget.request_complexity !== null ? { request_complexity: budget.request_complexity } : {}),
|
|
398
|
+
...(budget.reservations_active > 0 ? { shared_budget_reservations_active: budget.reservations_active } : {}),
|
|
399
|
+
...serializeLinearBudgetBucketDetails('requests', budget.requests),
|
|
400
|
+
...serializeLinearBudgetBucketDetails('endpoint_requests', budget.endpoint_requests),
|
|
401
|
+
...serializeLinearBudgetBucketDetails('complexity', budget.complexity),
|
|
402
|
+
...serializeLinearBudgetBucketDetails('endpoint_complexity', budget.endpoint_complexity),
|
|
403
|
+
...extraDetails
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
async function recordLinearBudgetObservation(input) {
|
|
407
|
+
const env = input.env ?? process.env;
|
|
408
|
+
const paths = resolveLinearBudgetStatePaths(env);
|
|
409
|
+
if (!paths) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
const observedAt = normalizeOptionalString(input.observedAt) ?? new Date().toISOString();
|
|
413
|
+
const observation = buildLinearBudgetObservation({
|
|
414
|
+
details: input.details,
|
|
415
|
+
source: input.source,
|
|
416
|
+
observedAt,
|
|
417
|
+
assumeUnknownResetsExhausted: input.assumeUnknownResetsExhausted
|
|
418
|
+
});
|
|
419
|
+
const lockScopeKey = await resolveLinearBudgetLockScopeKey(paths, input.scope);
|
|
420
|
+
return await withLinearBudgetStateLock(paths, async () => {
|
|
421
|
+
const existing = await readNewestPersistedLinearBudgetStatus(paths, normalizeScopeHint(input.scope));
|
|
422
|
+
const scope = await resolveWriteScope(paths, input.scope, existing);
|
|
423
|
+
const staleObservation = isStalePersistedLinearBudgetObservation(existing, observation);
|
|
424
|
+
const mergedWithoutHistory = mergePersistedLinearBudgetStatus(existing, observation, scope);
|
|
425
|
+
const merged = appendPersistedLinearBudgetRequestBurnHistory(mergedWithoutHistory, staleObservation
|
|
426
|
+
? null
|
|
427
|
+
: buildPersistedLinearBudgetRequestBurnHistoryEntry({
|
|
428
|
+
env,
|
|
429
|
+
existing,
|
|
430
|
+
observation,
|
|
431
|
+
merged: mergedWithoutHistory
|
|
432
|
+
}));
|
|
433
|
+
await writePersistedLinearBudgetStatus(paths, merged);
|
|
434
|
+
if (scope.kind === 'user') {
|
|
435
|
+
await writePersistedLinearBudgetAlias(paths, scope);
|
|
436
|
+
}
|
|
437
|
+
return hydrateLinearBudgetStatus(merged);
|
|
438
|
+
}, lockScopeKey);
|
|
439
|
+
}
|
|
440
|
+
function buildLinearBudgetObservation(input) {
|
|
441
|
+
const requests = parseLinearBudgetBucket(input.details, 'requests');
|
|
442
|
+
const complexity = parseLinearBudgetBucket(input.details, 'complexity');
|
|
443
|
+
const requestComplexity = parseNumberLike(input.details.request_complexity);
|
|
444
|
+
const endpointObservation = buildLinearBudgetEndpointObservation({
|
|
445
|
+
details: input.details,
|
|
446
|
+
source: input.source,
|
|
447
|
+
observedAt: input.observedAt,
|
|
448
|
+
assumeUnknownResetsExhausted: input.assumeUnknownResetsExhausted
|
|
449
|
+
});
|
|
450
|
+
return {
|
|
451
|
+
observed_at: input.observedAt,
|
|
452
|
+
source: input.source,
|
|
453
|
+
request_id: normalizeOptionalString(input.details.request_id),
|
|
454
|
+
retry_after_seconds: parseNumberLike(input.details.retry_after_seconds),
|
|
455
|
+
requests,
|
|
456
|
+
complexity,
|
|
457
|
+
request_complexity: requestComplexity,
|
|
458
|
+
endpoint: endpointObservation,
|
|
459
|
+
assume_unknown_resets_exhausted: input.assumeUnknownResetsExhausted
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function buildLinearBudgetEndpointObservation(input) {
|
|
463
|
+
const endpointName = normalizeOptionalString(input.details.endpoint_name);
|
|
464
|
+
const requests = parseLinearBudgetBucket(input.details, 'endpoint_requests');
|
|
465
|
+
const complexity = parseLinearBudgetBucket(input.details, 'endpoint_complexity');
|
|
466
|
+
const requestComplexity = parseNumberLike(input.details.request_complexity);
|
|
467
|
+
if (!endpointName && !requests && !complexity && requestComplexity === null) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
key: buildEndpointKey(endpointName, input.source),
|
|
472
|
+
endpoint_name: endpointName,
|
|
473
|
+
aliases: [input.source],
|
|
474
|
+
observed_at: input.observedAt,
|
|
475
|
+
requests,
|
|
476
|
+
complexity,
|
|
477
|
+
request_complexity: requestComplexity,
|
|
478
|
+
assume_unknown_resets_exhausted: input.assumeUnknownResetsExhausted
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function mergePersistedLinearBudgetStatus(existing, observation, scope) {
|
|
482
|
+
if (isStalePersistedLinearBudgetObservation(existing, observation)) {
|
|
483
|
+
return adoptPersistedScope(existing, scope);
|
|
484
|
+
}
|
|
485
|
+
const base = existing ? adoptPersistedScope(pruneExpiredReservations(existing), scope) : createEmptyPersistedLinearBudgetStatus(scope);
|
|
486
|
+
const endpoints = clonePersistedEndpoints(base.endpoints);
|
|
487
|
+
let selectedEndpointKey = base.selected_endpoint_key;
|
|
488
|
+
if (observation.endpoint) {
|
|
489
|
+
const upsertedEndpoint = upsertPersistedEndpointObservation(endpoints, observation.endpoint);
|
|
490
|
+
selectedEndpointKey = upsertedEndpoint.selectedEndpointKey;
|
|
491
|
+
}
|
|
492
|
+
const merged = {
|
|
493
|
+
...base,
|
|
494
|
+
observed_at: observation.observed_at,
|
|
495
|
+
source: observation.source,
|
|
496
|
+
request_id: observation.request_id,
|
|
497
|
+
retry_after_seconds: observation.retry_after_seconds,
|
|
498
|
+
requests: mergeLinearBudgetBucket(base.requests, observation.requests),
|
|
499
|
+
complexity: mergeLinearBudgetBucket(base.complexity, observation.complexity),
|
|
500
|
+
request_complexity: observation.request_complexity ?? base.request_complexity,
|
|
501
|
+
selected_endpoint_key: selectedEndpointKey,
|
|
502
|
+
endpoints,
|
|
503
|
+
reservations: base.reservations
|
|
504
|
+
};
|
|
505
|
+
merged.cooldown_until = resolveCooldownUntil({
|
|
506
|
+
persisted: merged,
|
|
507
|
+
observation
|
|
508
|
+
});
|
|
509
|
+
return merged;
|
|
510
|
+
}
|
|
511
|
+
function isStalePersistedLinearBudgetObservation(existing, observation) {
|
|
512
|
+
const existingObservedAtMs = existing ? parseIsoToMs(existing.observed_at) : null;
|
|
513
|
+
const observationObservedAtMs = parseIsoToMs(observation.observed_at);
|
|
514
|
+
return (existing !== null &&
|
|
515
|
+
existingObservedAtMs !== null &&
|
|
516
|
+
observationObservedAtMs !== null &&
|
|
517
|
+
observationObservedAtMs < existingObservedAtMs);
|
|
518
|
+
}
|
|
519
|
+
function createEmptyPersistedLinearBudgetStatus(scope) {
|
|
520
|
+
return {
|
|
521
|
+
schema_version: LINEAR_BUDGET_STATE_SCHEMA_VERSION,
|
|
522
|
+
scope_kind: scope.kind,
|
|
523
|
+
scope_key: scope.key,
|
|
524
|
+
viewer_id: scope.viewer_id,
|
|
525
|
+
workspace_id: scope.workspace_id,
|
|
526
|
+
token_fingerprints: [scope.token_fingerprint],
|
|
527
|
+
observed_at: new Date().toISOString(),
|
|
528
|
+
source: 'unknown',
|
|
529
|
+
request_id: null,
|
|
530
|
+
retry_after_seconds: null,
|
|
531
|
+
cooldown_until: null,
|
|
532
|
+
requests: null,
|
|
533
|
+
complexity: null,
|
|
534
|
+
request_complexity: null,
|
|
535
|
+
selected_endpoint_key: null,
|
|
536
|
+
endpoints: {},
|
|
537
|
+
reservations: [],
|
|
538
|
+
request_burn_history: []
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function adoptPersistedScope(persisted, scope) {
|
|
542
|
+
return {
|
|
543
|
+
...persisted,
|
|
544
|
+
scope_kind: scope.kind,
|
|
545
|
+
scope_key: scope.key,
|
|
546
|
+
viewer_id: scope.viewer_id ?? persisted.viewer_id,
|
|
547
|
+
workspace_id: scope.workspace_id ?? persisted.workspace_id,
|
|
548
|
+
token_fingerprints: uniqueStrings([...persisted.token_fingerprints, scope.token_fingerprint])
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
function clonePersistedLinearBudgetStatus(value) {
|
|
552
|
+
return {
|
|
553
|
+
...value,
|
|
554
|
+
token_fingerprints: [...value.token_fingerprints],
|
|
555
|
+
requests: cloneBucket(value.requests),
|
|
556
|
+
complexity: cloneBucket(value.complexity),
|
|
557
|
+
endpoints: clonePersistedEndpoints(value.endpoints),
|
|
558
|
+
reservations: value.reservations.map((entry) => ({ ...entry })),
|
|
559
|
+
request_burn_history: clonePersistedLinearBudgetRequestBurnHistory(value.request_burn_history)
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function clonePersistedEndpoints(value) {
|
|
563
|
+
return Object.fromEntries(Object.entries(value).map(([key, endpoint]) => [
|
|
564
|
+
key,
|
|
565
|
+
{
|
|
566
|
+
endpoint_name: endpoint.endpoint_name,
|
|
567
|
+
aliases: [...endpoint.aliases],
|
|
568
|
+
observed_at: endpoint.observed_at,
|
|
569
|
+
requests: cloneBucket(endpoint.requests),
|
|
570
|
+
complexity: cloneBucket(endpoint.complexity),
|
|
571
|
+
request_complexity: endpoint.request_complexity
|
|
572
|
+
}
|
|
573
|
+
]));
|
|
574
|
+
}
|
|
575
|
+
function upsertPersistedEndpointObservation(endpoints, observation) {
|
|
576
|
+
const targetKey = resolvePersistedEndpointTargetKey(endpoints, observation);
|
|
577
|
+
const existing = endpoints[targetKey] ?? null;
|
|
578
|
+
const merged = {
|
|
579
|
+
endpoint_name: observation.endpoint_name ?? existing?.endpoint_name ?? null,
|
|
580
|
+
aliases: uniqueStrings([...(existing?.aliases ?? []), ...observation.aliases]),
|
|
581
|
+
observed_at: maxIsoTimestamp(existing?.observed_at ?? null, observation.observed_at) ?? observation.observed_at,
|
|
582
|
+
requests: mergeLinearBudgetBucket(existing?.requests ?? null, observation.requests),
|
|
583
|
+
complexity: mergeLinearBudgetBucket(existing?.complexity ?? null, observation.complexity),
|
|
584
|
+
request_complexity: observation.request_complexity ?? existing?.request_complexity ?? null
|
|
585
|
+
};
|
|
586
|
+
for (const [key, endpoint] of Object.entries(endpoints)) {
|
|
587
|
+
if (key === targetKey) {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if ((observation.endpoint_name && endpoint.endpoint_name === observation.endpoint_name) ||
|
|
591
|
+
endpoint.aliases.some((alias) => observation.aliases.includes(alias))) {
|
|
592
|
+
merged.aliases = uniqueStrings([...merged.aliases, ...endpoint.aliases]);
|
|
593
|
+
merged.requests = mergeLinearBudgetBucket(merged.requests, endpoint.requests);
|
|
594
|
+
merged.complexity = mergeLinearBudgetBucket(merged.complexity, endpoint.complexity);
|
|
595
|
+
merged.request_complexity = merged.request_complexity ?? endpoint.request_complexity ?? null;
|
|
596
|
+
merged.observed_at = maxIsoTimestamp(merged.observed_at, endpoint.observed_at) ?? merged.observed_at;
|
|
597
|
+
delete endpoints[key];
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
endpoints[targetKey] = merged;
|
|
601
|
+
return {
|
|
602
|
+
selectedEndpointKey: targetKey
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function resolvePersistedEndpointTargetKey(endpoints, observation) {
|
|
606
|
+
if (endpoints[observation.key]) {
|
|
607
|
+
return observation.key;
|
|
608
|
+
}
|
|
609
|
+
if (observation.endpoint_name) {
|
|
610
|
+
for (const [key, endpoint] of Object.entries(endpoints)) {
|
|
611
|
+
if (endpoint.endpoint_name === observation.endpoint_name) {
|
|
612
|
+
return key;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
for (const [key, endpoint] of Object.entries(endpoints)) {
|
|
617
|
+
if (endpoint.aliases.some((alias) => observation.aliases.includes(alias))) {
|
|
618
|
+
return observation.endpoint_name ? buildEndpointKey(observation.endpoint_name, observation.aliases[0] ?? key) : key;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return observation.key;
|
|
622
|
+
}
|
|
623
|
+
function mergePersistedLinearBudgetReservations(existing, candidate) {
|
|
624
|
+
if (candidate.scope_kind === 'user') {
|
|
625
|
+
return clonePersistedLinearBudgetReservations(candidate.reservations);
|
|
626
|
+
}
|
|
627
|
+
if (existing.scope_kind === 'user') {
|
|
628
|
+
return clonePersistedLinearBudgetReservations(existing.reservations);
|
|
629
|
+
}
|
|
630
|
+
return clonePersistedLinearBudgetReservations(candidate.reservations);
|
|
631
|
+
}
|
|
632
|
+
function comparePersistedReservationCreatedAt(left, right) {
|
|
633
|
+
const leftCreatedAtMs = parseIsoToMs(left.created_at) ?? Number.NEGATIVE_INFINITY;
|
|
634
|
+
const rightCreatedAtMs = parseIsoToMs(right.created_at) ?? Number.NEGATIVE_INFINITY;
|
|
635
|
+
return leftCreatedAtMs - rightCreatedAtMs;
|
|
636
|
+
}
|
|
637
|
+
function clonePersistedLinearBudgetReservations(value) {
|
|
638
|
+
return value.map((entry) => ({ ...entry })).sort(comparePersistedReservationCreatedAt);
|
|
639
|
+
}
|
|
640
|
+
function clonePersistedLinearBudgetRequestBurnHistory(value) {
|
|
641
|
+
return [...(value ?? [])]
|
|
642
|
+
.map((entry) => ({ ...entry }))
|
|
643
|
+
.sort(comparePersistedLinearBudgetRequestBurnHistoryEntry);
|
|
644
|
+
}
|
|
645
|
+
function comparePersistedLinearBudgetRequestBurnHistoryEntry(left, right) {
|
|
646
|
+
const leftRecordedAtMs = parseIsoToMs(left.recorded_at) ?? parseIsoToMs(left.observed_at) ?? Number.NEGATIVE_INFINITY;
|
|
647
|
+
const rightRecordedAtMs = parseIsoToMs(right.recorded_at) ?? parseIsoToMs(right.observed_at) ?? Number.NEGATIVE_INFINITY;
|
|
648
|
+
return leftRecordedAtMs - rightRecordedAtMs;
|
|
649
|
+
}
|
|
650
|
+
function mergePersistedLinearBudgetRequestBurnHistory(existing, candidate) {
|
|
651
|
+
const deduped = new Map();
|
|
652
|
+
for (const entry of [...(existing ?? []), ...(candidate ?? [])]) {
|
|
653
|
+
const key = [
|
|
654
|
+
entry.recorded_at,
|
|
655
|
+
entry.observed_at,
|
|
656
|
+
entry.source,
|
|
657
|
+
entry.request_id ?? '',
|
|
658
|
+
entry.request_bucket ?? '',
|
|
659
|
+
entry.remaining ?? '',
|
|
660
|
+
entry.reset_at ?? ''
|
|
661
|
+
].join('|');
|
|
662
|
+
deduped.set(key, { ...entry });
|
|
663
|
+
}
|
|
664
|
+
return [...deduped.values()]
|
|
665
|
+
.sort(comparePersistedLinearBudgetRequestBurnHistoryEntry)
|
|
666
|
+
.slice(-LINEAR_BUDGET_REQUEST_BURN_HISTORY_LIMIT);
|
|
667
|
+
}
|
|
668
|
+
function appendPersistedLinearBudgetRequestBurnHistory(persisted, entry) {
|
|
669
|
+
if (!entry) {
|
|
670
|
+
return persisted;
|
|
671
|
+
}
|
|
672
|
+
const requestBurnHistory = mergePersistedLinearBudgetRequestBurnHistory(persisted.request_burn_history, [entry]);
|
|
673
|
+
return {
|
|
674
|
+
...persisted,
|
|
675
|
+
request_burn_history: requestBurnHistory
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function buildPersistedLinearBudgetRequestBurnHistoryEntry(input) {
|
|
679
|
+
const requestSample = resolveLinearBudgetRequestBurnSample(input.existing, input.observation);
|
|
680
|
+
if (!requestSample) {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
const hydrated = hydrateLinearBudgetStatus(input.merged, {
|
|
684
|
+
operation: input.observation.source
|
|
685
|
+
});
|
|
686
|
+
return {
|
|
687
|
+
recorded_at: new Date().toISOString(),
|
|
688
|
+
observed_at: input.observation.observed_at,
|
|
689
|
+
source: input.observation.source,
|
|
690
|
+
operation: normalizeOptionalString(input.observation.source) ?? 'unknown',
|
|
691
|
+
run_id: resolveLinearBudgetObservationRunId(input.env),
|
|
692
|
+
process_pid: process.pid,
|
|
693
|
+
process_title: normalizeOptionalString(process.title),
|
|
694
|
+
request_id: input.observation.request_id,
|
|
695
|
+
request_bucket: requestSample.bucket,
|
|
696
|
+
remaining: requestSample.remaining,
|
|
697
|
+
remaining_delta: requestSample.remaining_delta,
|
|
698
|
+
reset_at: requestSample.reset_at,
|
|
699
|
+
suppression_reason: hydrated.suppression_reason,
|
|
700
|
+
cooldown_reason: hydrated.cooldown_active ? hydrated.suppression_reason : null
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function resolveLinearBudgetRequestBurnSample(existing, observation) {
|
|
704
|
+
if (observation.requests) {
|
|
705
|
+
return {
|
|
706
|
+
bucket: 'requests',
|
|
707
|
+
remaining: observation.requests.remaining,
|
|
708
|
+
remaining_delta: resolveLinearBudgetRemainingDelta(existing?.requests?.remaining ?? null, observation.requests.remaining),
|
|
709
|
+
reset_at: observation.requests.reset_at
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
if (observation.endpoint?.requests) {
|
|
713
|
+
const targetKey = existing?.endpoints
|
|
714
|
+
? resolvePersistedEndpointTargetKey(clonePersistedEndpoints(existing.endpoints), observation.endpoint)
|
|
715
|
+
: observation.endpoint.key;
|
|
716
|
+
return {
|
|
717
|
+
bucket: 'endpoint_requests',
|
|
718
|
+
remaining: observation.endpoint.requests.remaining,
|
|
719
|
+
remaining_delta: resolveLinearBudgetRemainingDelta(existing?.endpoints[targetKey]?.requests?.remaining ?? null, observation.endpoint.requests.remaining),
|
|
720
|
+
reset_at: observation.endpoint.requests.reset_at
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
function resolveLinearBudgetRemainingDelta(previousRemaining, nextRemaining) {
|
|
726
|
+
return previousRemaining !== null && nextRemaining !== null ? nextRemaining - previousRemaining : null;
|
|
727
|
+
}
|
|
728
|
+
function resolveLinearBudgetObservationRunId(env) {
|
|
729
|
+
return normalizeOptionalString(env.CODEX_ORCHESTRATOR_RUN_ID)
|
|
730
|
+
?? normalizeOptionalString(env.CODEX_ORCHESTRATOR_PROVIDER_CONTROL_HOST_RUN_ID);
|
|
731
|
+
}
|
|
732
|
+
function hydrateLinearBudgetStatus(persisted, options = {}) {
|
|
733
|
+
const cleanedReservations = pruneExpiredReservations(persisted).reservations;
|
|
734
|
+
const normalizedEndpoints = Object.fromEntries(Object.entries(persisted.endpoints).map(([key, endpoint]) => [
|
|
735
|
+
key,
|
|
736
|
+
{
|
|
737
|
+
key,
|
|
738
|
+
endpoint_name: endpoint.endpoint_name,
|
|
739
|
+
aliases: [...endpoint.aliases],
|
|
740
|
+
observed_at: endpoint.observed_at,
|
|
741
|
+
requests: normalizeExpiredLinearBudgetBucket(endpoint.requests, endpoint.observed_at, {
|
|
742
|
+
requestReserveBucket: 'endpoint_requests'
|
|
743
|
+
}),
|
|
744
|
+
complexity: normalizeExpiredLinearBudgetBucket(endpoint.complexity, endpoint.observed_at),
|
|
745
|
+
request_complexity: endpoint.request_complexity
|
|
746
|
+
}
|
|
747
|
+
]));
|
|
748
|
+
const base = {
|
|
749
|
+
observed_at: persisted.observed_at,
|
|
750
|
+
source: persisted.source,
|
|
751
|
+
request_id: persisted.request_id,
|
|
752
|
+
retry_after_seconds: persisted.retry_after_seconds,
|
|
753
|
+
cooldown_until: persisted.cooldown_until,
|
|
754
|
+
cooldown_active: isFutureIsoTimestamp(persisted.cooldown_until),
|
|
755
|
+
suppression: 'none',
|
|
756
|
+
suppression_reason: null,
|
|
757
|
+
scope_kind: persisted.scope_kind,
|
|
758
|
+
scope_key: persisted.scope_key,
|
|
759
|
+
viewer_id: persisted.viewer_id,
|
|
760
|
+
workspace_id: persisted.workspace_id,
|
|
761
|
+
token_fingerprints: [...persisted.token_fingerprints],
|
|
762
|
+
requests: normalizeExpiredLinearBudgetBucket(persisted.requests, persisted.observed_at, {
|
|
763
|
+
requestReserveBucket: 'requests'
|
|
764
|
+
}),
|
|
765
|
+
endpoint_requests: null,
|
|
766
|
+
complexity: normalizeExpiredLinearBudgetBucket(persisted.complexity, persisted.observed_at),
|
|
767
|
+
endpoint_complexity: null,
|
|
768
|
+
endpoint_name: null,
|
|
769
|
+
selected_endpoint_key: persisted.selected_endpoint_key,
|
|
770
|
+
request_complexity: persisted.request_complexity,
|
|
771
|
+
endpoints: normalizedEndpoints,
|
|
772
|
+
reservations: cleanedReservations.map((entry) => ({ ...entry })),
|
|
773
|
+
request_burn_history: clonePersistedLinearBudgetRequestBurnHistory(persisted.request_burn_history),
|
|
774
|
+
reservations_active: cleanedReservations.length
|
|
775
|
+
};
|
|
776
|
+
const materialized = materializeBudgetForOperation(base, options.operation ?? null);
|
|
777
|
+
if (materialized.cooldown_active) {
|
|
778
|
+
return {
|
|
779
|
+
...materialized,
|
|
780
|
+
suppression: 'cooldown',
|
|
781
|
+
suppression_reason: 'linear_budget_shared_cooldown'
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
const pressure = resolveMaterializedBudgetPressure(materialized);
|
|
785
|
+
return {
|
|
786
|
+
...materialized,
|
|
787
|
+
suppression: pressure.suppression,
|
|
788
|
+
suppression_reason: pressure.reason
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
function materializeBudgetForOperation(budget, operation) {
|
|
792
|
+
const view = resolveLinearBudgetOperationView(budget, operation ?? null);
|
|
793
|
+
return {
|
|
794
|
+
...budget,
|
|
795
|
+
requests: view.requests,
|
|
796
|
+
endpoint_requests: view.endpoint_requests,
|
|
797
|
+
complexity: view.complexity,
|
|
798
|
+
endpoint_complexity: view.endpoint_complexity,
|
|
799
|
+
endpoint_name: view.endpoint_name,
|
|
800
|
+
selected_endpoint_key: view.endpoint_key,
|
|
801
|
+
request_complexity: view.request_complexity,
|
|
802
|
+
reservations_active: view.reservations_active
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
function resolveLinearBudgetOperationView(budget, operation) {
|
|
806
|
+
const normalizedOperation = normalizeOptionalString(operation);
|
|
807
|
+
const matchedEndpointKeys = normalizedOperation
|
|
808
|
+
? resolveMatchingEndpointKeys(budget.endpoints, normalizedOperation)
|
|
809
|
+
: null;
|
|
810
|
+
const reservations = budget.reservations.filter((entry) => isFutureIsoTimestamp(entry.expires_at));
|
|
811
|
+
const selectedEndpointKey = resolveSelectedEndpointKey(budget, normalizedOperation, matchedEndpointKeys, reservations);
|
|
812
|
+
const selectedEndpoint = selectedEndpointKey ? budget.endpoints[selectedEndpointKey] ?? null : null;
|
|
813
|
+
const globalRequestsReserved = reservations.reduce((sum, entry) => sum + entry.requests, 0);
|
|
814
|
+
const globalComplexityReserved = reservations.reduce((sum, entry) => sum + (entry.complexity ?? 0), 0);
|
|
815
|
+
const endpointReservations = selectedEndpointKey
|
|
816
|
+
? reservations.filter((entry) => entry.endpoint_key === selectedEndpointKey)
|
|
817
|
+
: [];
|
|
818
|
+
const endpointRequestsReserved = endpointReservations.reduce((sum, entry) => sum + entry.requests, 0);
|
|
819
|
+
const endpointComplexityReserved = endpointReservations.reduce((sum, entry) => sum + (entry.complexity ?? 0), 0);
|
|
820
|
+
return {
|
|
821
|
+
requests: subtractReservationFromBucket(budget.requests, globalRequestsReserved),
|
|
822
|
+
endpoint_requests: subtractReservationFromBucket(selectedEndpoint?.requests ?? null, endpointRequestsReserved),
|
|
823
|
+
complexity: subtractReservationFromBucket(budget.complexity, globalComplexityReserved),
|
|
824
|
+
endpoint_complexity: subtractReservationFromBucket(selectedEndpoint?.complexity ?? null, endpointComplexityReserved),
|
|
825
|
+
endpoint_key: selectedEndpointKey,
|
|
826
|
+
endpoint_name: selectedEndpoint?.endpoint_name ?? null,
|
|
827
|
+
request_complexity: resolveOperationRequestComplexity(budget, normalizedOperation, selectedEndpoint, matchedEndpointKeys),
|
|
828
|
+
reservations_active: reservations.length,
|
|
829
|
+
reservation_count: selectedEndpoint ? endpointReservations.length : reservations.length
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
function resolveSelectedEndpointKey(budget, operation, matchedEndpointKeys = null, reservations = []) {
|
|
833
|
+
if (operation) {
|
|
834
|
+
return selectMostConstrainedEndpointKey(budget.endpoints, matchedEndpointKeys ?? [], reservations);
|
|
835
|
+
}
|
|
836
|
+
if (budget.selected_endpoint_key && budget.endpoints[budget.selected_endpoint_key]) {
|
|
837
|
+
return budget.selected_endpoint_key;
|
|
838
|
+
}
|
|
839
|
+
return selectMostConstrainedEndpointKey(budget.endpoints);
|
|
840
|
+
}
|
|
841
|
+
function selectMostConstrainedEndpointKey(endpoints, candidateKeys = Object.keys(endpoints), reservations = []) {
|
|
842
|
+
let selected = { key: null, rank: -1, headroom: Number.POSITIVE_INFINITY };
|
|
843
|
+
for (const key of candidateKeys) {
|
|
844
|
+
const endpoint = endpoints[key];
|
|
845
|
+
if (!endpoint) {
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
const endpointReservations = reservations.filter((entry) => entry.endpoint_key === key);
|
|
849
|
+
const requestsReserved = endpointReservations.reduce((sum, entry) => sum + entry.requests, 0);
|
|
850
|
+
const complexityReserved = endpointReservations.reduce((sum, entry) => sum + (entry.complexity ?? 0), 0);
|
|
851
|
+
const pressure = resolveEndpointPressure({
|
|
852
|
+
requests: subtractReservationFromBucket(endpoint.requests, requestsReserved),
|
|
853
|
+
complexity: subtractReservationFromBucket(endpoint.complexity, complexityReserved)
|
|
854
|
+
});
|
|
855
|
+
if (pressure.rank > selected.rank ||
|
|
856
|
+
(pressure.rank === selected.rank && pressure.headroom < selected.headroom)) {
|
|
857
|
+
selected = {
|
|
858
|
+
key,
|
|
859
|
+
rank: pressure.rank,
|
|
860
|
+
headroom: pressure.headroom
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return selected.key;
|
|
865
|
+
}
|
|
866
|
+
function resolveMatchingEndpointKeys(endpoints, operation) {
|
|
867
|
+
const matches = new Set();
|
|
868
|
+
const sourceKey = buildEndpointKey(null, operation);
|
|
869
|
+
if (endpoints[sourceKey]) {
|
|
870
|
+
matches.add(sourceKey);
|
|
871
|
+
}
|
|
872
|
+
const operationPrefix = `${operation}:`;
|
|
873
|
+
for (const [key, endpoint] of Object.entries(endpoints)) {
|
|
874
|
+
if (endpoint.aliases.some((alias) => alias === operation ||
|
|
875
|
+
alias.startsWith(operationPrefix) ||
|
|
876
|
+
operation.startsWith(`${alias}:`))) {
|
|
877
|
+
matches.add(key);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return [...matches];
|
|
881
|
+
}
|
|
882
|
+
function resolveOperationRequestComplexity(budget, operation, selectedEndpoint, matchedEndpointKeys) {
|
|
883
|
+
if (!operation) {
|
|
884
|
+
return selectedEndpoint?.request_complexity ?? budget.request_complexity;
|
|
885
|
+
}
|
|
886
|
+
const matchedComplexities = (matchedEndpointKeys ?? [])
|
|
887
|
+
.map((key) => budget.endpoints[key]?.request_complexity ?? null)
|
|
888
|
+
.filter((value) => value !== null);
|
|
889
|
+
if (matchedComplexities.length === 0) {
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
return Math.max(...matchedComplexities);
|
|
893
|
+
}
|
|
894
|
+
function resolveMaterializedBudgetPressure(budget) {
|
|
895
|
+
let selected = resolveBucketPressure('requests', budget.requests, false);
|
|
896
|
+
for (const next of [
|
|
897
|
+
resolveBucketPressure('complexity', budget.complexity, false),
|
|
898
|
+
resolveBucketPressure('endpoint_requests', budget.endpoint_requests, true),
|
|
899
|
+
resolveBucketPressure('endpoint_complexity', budget.endpoint_complexity, true)
|
|
900
|
+
]) {
|
|
901
|
+
if (next.rank > selected.rank) {
|
|
902
|
+
selected = next;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return selected;
|
|
906
|
+
}
|
|
907
|
+
function resolveEndpointPressure(endpoint) {
|
|
908
|
+
const selected = [
|
|
909
|
+
resolveBucketPressure('endpoint_requests', endpoint.requests, true),
|
|
910
|
+
resolveBucketPressure('endpoint_complexity', endpoint.complexity, true)
|
|
911
|
+
].reduce((selected, next) => (next.rank > selected.rank ? next : selected), {
|
|
912
|
+
rank: 0,
|
|
913
|
+
suppression: 'none',
|
|
914
|
+
reason: null,
|
|
915
|
+
endpoint_specific: true
|
|
916
|
+
});
|
|
917
|
+
return {
|
|
918
|
+
rank: selected.rank,
|
|
919
|
+
headroom: resolveEndpointHeadroom(endpoint.requests, endpoint.complexity)
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
function resolveEndpointHeadroom(requests, complexity) {
|
|
923
|
+
const candidates = [requests?.remaining ?? null, complexity?.remaining ?? null].filter((value) => value !== null);
|
|
924
|
+
return candidates.length > 0 ? Math.min(...candidates) : Number.POSITIVE_INFINITY;
|
|
925
|
+
}
|
|
926
|
+
function resolveBucketPressure(bucketKey, bucket, endpointSpecific) {
|
|
927
|
+
if (!bucket || bucket.remaining === null) {
|
|
928
|
+
return { rank: 0, suppression: 'none', reason: null, endpoint_specific: endpointSpecific };
|
|
929
|
+
}
|
|
930
|
+
if (bucket.remaining <= 0) {
|
|
931
|
+
return {
|
|
932
|
+
rank: endpointSpecific ? 4 : 3,
|
|
933
|
+
suppression: 'exhausted',
|
|
934
|
+
reason: bucketKey === 'endpoint_requests' || bucketKey === 'endpoint_complexity'
|
|
935
|
+
? `linear_budget_${bucketKey}_exhausted`
|
|
936
|
+
: `linear_budget_${bucketKey}_exhausted`,
|
|
937
|
+
endpoint_specific: endpointSpecific
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
const limit = bucket.limit;
|
|
941
|
+
if (limit === null || limit <= 0) {
|
|
942
|
+
return { rank: 0, suppression: 'none', reason: null, endpoint_specific: endpointSpecific };
|
|
943
|
+
}
|
|
944
|
+
const lowThreshold = Math.max(1, Math.min(10, Math.floor(limit * 0.02)));
|
|
945
|
+
const constrainedThreshold = Math.max(lowThreshold + 1, Math.min(50, Math.floor(limit * 0.1)));
|
|
946
|
+
if (bucket.remaining <= lowThreshold) {
|
|
947
|
+
return {
|
|
948
|
+
rank: endpointSpecific ? 3 : 2,
|
|
949
|
+
suppression: 'low',
|
|
950
|
+
reason: `linear_budget_${bucketKey}_low`,
|
|
951
|
+
endpoint_specific: endpointSpecific
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
if (bucket.remaining <= constrainedThreshold) {
|
|
955
|
+
return {
|
|
956
|
+
rank: endpointSpecific ? 2 : 1,
|
|
957
|
+
suppression: 'constrained',
|
|
958
|
+
reason: `linear_budget_${bucketKey}_constrained`,
|
|
959
|
+
endpoint_specific: endpointSpecific
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
return { rank: 0, suppression: 'none', reason: null, endpoint_specific: endpointSpecific };
|
|
963
|
+
}
|
|
964
|
+
function resolveRequestPollingHeadroomGuard(input) {
|
|
965
|
+
let selected = null;
|
|
966
|
+
for (const candidate of [
|
|
967
|
+
resolveRequestBucketPollingHeadroomGuard({
|
|
968
|
+
bucket: input.budget.requests,
|
|
969
|
+
bucketKey: 'requests',
|
|
970
|
+
endpointSpecific: false,
|
|
971
|
+
defaultIntervalMs: input.defaultIntervalMs,
|
|
972
|
+
nowMs: input.nowMs
|
|
973
|
+
}),
|
|
974
|
+
resolveRequestBucketPollingHeadroomGuard({
|
|
975
|
+
bucket: input.budget.endpoint_requests,
|
|
976
|
+
bucketKey: 'endpoint_requests',
|
|
977
|
+
endpointSpecific: true,
|
|
978
|
+
defaultIntervalMs: input.defaultIntervalMs,
|
|
979
|
+
nowMs: input.nowMs
|
|
980
|
+
})
|
|
981
|
+
]) {
|
|
982
|
+
if (!candidate) {
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
if (!selected || candidate.interval_ms > selected.interval_ms) {
|
|
986
|
+
selected = candidate;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return selected;
|
|
990
|
+
}
|
|
991
|
+
function resolveRequestBucketPollingHeadroomGuard(input) {
|
|
992
|
+
const bucket = input.bucket;
|
|
993
|
+
if (!bucket || bucket.remaining === null || bucket.remaining <= 0) {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
const resetAtMs = parseIsoToMs(bucket.reset_at);
|
|
997
|
+
if (resetAtMs === null || resetAtMs <= input.nowMs) {
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
const reserve = resolveRequestPollingHeadroomReserve(bucket.limit, input.endpointSpecific);
|
|
1001
|
+
const usableRemaining = Math.max(1, bucket.remaining - reserve);
|
|
1002
|
+
const intervalFloorMs = Math.ceil((resetAtMs - input.nowMs) / usableRemaining);
|
|
1003
|
+
if (!Number.isFinite(intervalFloorMs) || intervalFloorMs <= input.defaultIntervalMs) {
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
const lowIntervalMs = input.endpointSpecific
|
|
1007
|
+
? LINEAR_BUDGET_DEFAULT_ENDPOINT_LOW_POLL_INTERVAL_MS
|
|
1008
|
+
: LINEAR_BUDGET_DEFAULT_LOW_POLL_INTERVAL_MS;
|
|
1009
|
+
return {
|
|
1010
|
+
interval_ms: intervalFloorMs,
|
|
1011
|
+
suppression: intervalFloorMs >= lowIntervalMs ? 'low' : 'constrained',
|
|
1012
|
+
reason: `linear_budget_${input.bucketKey}_reset_headroom`,
|
|
1013
|
+
endpoint_specific: input.endpointSpecific
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function resolveRequestPollingHeadroomReserve(limit, endpointSpecific) {
|
|
1017
|
+
if (limit === null || limit <= 0) {
|
|
1018
|
+
return 1;
|
|
1019
|
+
}
|
|
1020
|
+
const ratio = endpointSpecific
|
|
1021
|
+
? LINEAR_BUDGET_ENDPOINT_REQUEST_HEADROOM_RESERVE_RATIO
|
|
1022
|
+
: LINEAR_BUDGET_REQUEST_HEADROOM_RESERVE_RATIO;
|
|
1023
|
+
const maxReserve = endpointSpecific
|
|
1024
|
+
? LINEAR_BUDGET_ENDPOINT_REQUEST_HEADROOM_RESERVE_MAX
|
|
1025
|
+
: LINEAR_BUDGET_REQUEST_HEADROOM_RESERVE_MAX;
|
|
1026
|
+
return Math.max(1, Math.min(maxReserve, Math.floor(limit * ratio)));
|
|
1027
|
+
}
|
|
1028
|
+
function inferOperationComplexityFloor(requestComplexity, minimumRequestsRemaining) {
|
|
1029
|
+
if (requestComplexity === null) {
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
return Math.max(1, requestComplexity * Math.max(1, minimumRequestsRemaining));
|
|
1033
|
+
}
|
|
1034
|
+
function resolveBucketShortfall(entries, requiredRemaining) {
|
|
1035
|
+
for (const [bucketKey, bucket] of entries) {
|
|
1036
|
+
if (bucket && bucket.remaining !== null && bucket.remaining < requiredRemaining) {
|
|
1037
|
+
return {
|
|
1038
|
+
bucket: bucketKey,
|
|
1039
|
+
remaining: bucket.remaining
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
function resolveRequestReserveShortfall(entries, requiredRemaining) {
|
|
1046
|
+
for (const [bucketKey, bucket] of entries) {
|
|
1047
|
+
if (!bucket || bucket.remaining === null) {
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
const reserve = resolveRequestPollingHeadroomReserve(bucket.limit, bucketKey === 'endpoint_requests');
|
|
1051
|
+
const usableRemaining = bucket.remaining - reserve;
|
|
1052
|
+
if (usableRemaining < requiredRemaining) {
|
|
1053
|
+
return {
|
|
1054
|
+
bucket: bucketKey,
|
|
1055
|
+
remaining: bucket.remaining,
|
|
1056
|
+
reserve,
|
|
1057
|
+
usable_remaining: usableRemaining
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
function findExhaustedLinearBudgetBucket(budget) {
|
|
1064
|
+
for (const [bucketKey, bucket] of [
|
|
1065
|
+
['requests', budget.requests],
|
|
1066
|
+
['endpoint_requests', budget.endpoint_requests],
|
|
1067
|
+
['complexity', budget.complexity],
|
|
1068
|
+
['endpoint_complexity', budget.endpoint_complexity]
|
|
1069
|
+
]) {
|
|
1070
|
+
if (bucket && bucket.remaining !== null && bucket.remaining <= 0) {
|
|
1071
|
+
return bucketKey;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
function parseLinearBudgetBucket(details, bucketKey) {
|
|
1077
|
+
const limit = parseNumberLike(details[`${bucketKey}_limit`]);
|
|
1078
|
+
const remaining = parseNumberLike(details[`${bucketKey}_remaining`]);
|
|
1079
|
+
const resetAt = normalizeOptionalString(details[`${bucketKey}_reset_at`]);
|
|
1080
|
+
if (limit === null && remaining === null && resetAt === null) {
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
return {
|
|
1084
|
+
limit,
|
|
1085
|
+
remaining,
|
|
1086
|
+
reset_at: resetAt
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
function resolveCooldownUntil(input) {
|
|
1090
|
+
const candidateMs = collectPersistedLinearBudgetCooldownCandidates({
|
|
1091
|
+
persisted: input.persisted,
|
|
1092
|
+
observedAt: input.observation.observed_at,
|
|
1093
|
+
retryAfterSeconds: input.observation.retry_after_seconds
|
|
1094
|
+
});
|
|
1095
|
+
if (input.observation.assume_unknown_resets_exhausted) {
|
|
1096
|
+
maybeCollectResetCandidate(candidateMs, input.observation.requests, true);
|
|
1097
|
+
maybeCollectResetCandidate(candidateMs, input.observation.complexity, true);
|
|
1098
|
+
}
|
|
1099
|
+
return finalizeCooldownCandidateMs(candidateMs);
|
|
1100
|
+
}
|
|
1101
|
+
function resolvePersistedLinearBudgetCooldownUntil(input) {
|
|
1102
|
+
return finalizeCooldownCandidateMs(collectPersistedLinearBudgetCooldownCandidates({
|
|
1103
|
+
persisted: input.persisted,
|
|
1104
|
+
observedAt: input.observedAt,
|
|
1105
|
+
retryAfterSeconds: input.retryAfterSeconds
|
|
1106
|
+
}));
|
|
1107
|
+
}
|
|
1108
|
+
function collectPersistedLinearBudgetCooldownCandidates(input) {
|
|
1109
|
+
const candidateMs = [];
|
|
1110
|
+
const observedAtMs = parseIsoToMs(input.observedAt);
|
|
1111
|
+
if (observedAtMs !== null && input.retryAfterSeconds !== null && input.retryAfterSeconds >= 0) {
|
|
1112
|
+
candidateMs.push(observedAtMs + input.retryAfterSeconds * 1000);
|
|
1113
|
+
}
|
|
1114
|
+
for (const bucket of [input.persisted.requests, input.persisted.complexity]) {
|
|
1115
|
+
maybeCollectResetCandidate(candidateMs, bucket, false);
|
|
1116
|
+
}
|
|
1117
|
+
return candidateMs;
|
|
1118
|
+
}
|
|
1119
|
+
function finalizeCooldownCandidateMs(candidateMs) {
|
|
1120
|
+
if (candidateMs.length === 0) {
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
1123
|
+
const latestMs = Math.max(...candidateMs);
|
|
1124
|
+
return Number.isFinite(latestMs) ? new Date(latestMs).toISOString() : null;
|
|
1125
|
+
}
|
|
1126
|
+
function maybeCollectResetCandidate(candidates, bucket, assumeUnknownRemainingExhausted) {
|
|
1127
|
+
if (!bucket?.reset_at) {
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const resetAtMs = parseIsoToMs(bucket.reset_at);
|
|
1131
|
+
if (resetAtMs === null) {
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
const exhausted = bucket.remaining !== null ? bucket.remaining <= 0 : assumeUnknownRemainingExhausted;
|
|
1135
|
+
if (exhausted) {
|
|
1136
|
+
candidates.push(resetAtMs);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
function serializeLinearBudgetBucketDetails(bucketKey, bucket) {
|
|
1140
|
+
if (!bucket) {
|
|
1141
|
+
return {};
|
|
1142
|
+
}
|
|
1143
|
+
return {
|
|
1144
|
+
...(bucket.limit !== null ? { [`${bucketKey}_limit`]: bucket.limit } : {}),
|
|
1145
|
+
...(bucket.remaining !== null ? { [`${bucketKey}_remaining`]: bucket.remaining } : {}),
|
|
1146
|
+
...(bucket.reset_at ? { [`${bucketKey}_reset_at`]: bucket.reset_at } : {})
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
function resolveLinearBudgetStatePaths(env) {
|
|
1150
|
+
const tokenFingerprint = resolveLinearApiTokenFingerprint(env);
|
|
1151
|
+
if (!tokenFingerprint) {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
const isVitestRuntime = normalizeOptionalString(env.VITEST) === 'true' ||
|
|
1155
|
+
normalizeOptionalString(process.env.VITEST) === 'true' ||
|
|
1156
|
+
normalizeOptionalString(env.NODE_ENV) === 'test' ||
|
|
1157
|
+
normalizeOptionalString(process.env.NODE_ENV) === 'test';
|
|
1158
|
+
if (isVitestRuntime && !normalizeOptionalString(env.CODEX_HOME)) {
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
const directory = join(resolveCodexOrchestratorHome(env), LINEAR_BUDGET_STATE_DIRNAME);
|
|
1162
|
+
return {
|
|
1163
|
+
directory,
|
|
1164
|
+
scopesDir: join(directory, LINEAR_BUDGET_SCOPE_DIRNAME),
|
|
1165
|
+
aliasesDir: join(directory, LINEAR_BUDGET_ALIAS_DIRNAME),
|
|
1166
|
+
legacyStatePath: join(directory, `${tokenFingerprint}.json`),
|
|
1167
|
+
aliasPath: join(directory, LINEAR_BUDGET_ALIAS_DIRNAME, `${tokenFingerprint}.json`),
|
|
1168
|
+
lockPath: join(directory, `${tokenFingerprint}.lock`),
|
|
1169
|
+
tokenFingerprint
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
async function withLinearBudgetStateLock(paths, callback, lockScopeKey = paths.tokenFingerprint) {
|
|
1173
|
+
const lock = await acquireLockWithRetry({
|
|
1174
|
+
taskId: `linear-budget-${lockScopeKey.slice(0, 12)}`,
|
|
1175
|
+
lockPath: resolveLinearBudgetLockPath(paths, lockScopeKey),
|
|
1176
|
+
retry: LINEAR_BUDGET_LOCK_RETRY,
|
|
1177
|
+
ensureDirectory: async () => {
|
|
1178
|
+
await mkdir(dirname(resolveLinearBudgetLockPath(paths, lockScopeKey)), { recursive: true });
|
|
1179
|
+
},
|
|
1180
|
+
createError: (taskId, attempts) => new Error(`Failed to acquire Linear budget state lock for ${taskId} after ${attempts} attempts.`)
|
|
1181
|
+
});
|
|
1182
|
+
try {
|
|
1183
|
+
return await callback();
|
|
1184
|
+
}
|
|
1185
|
+
finally {
|
|
1186
|
+
await lock.release();
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
async function resolveLinearBudgetLockScopeKey(paths, scopeHint = undefined) {
|
|
1190
|
+
const normalizedHint = normalizeScopeHint(scopeHint);
|
|
1191
|
+
if (normalizedHint?.viewer_id) {
|
|
1192
|
+
return resolveUserScopeKey(normalizedHint.viewer_id, normalizedHint.workspace_id);
|
|
1193
|
+
}
|
|
1194
|
+
const alias = await readPersistedLinearBudgetAlias(paths.aliasPath, paths.tokenFingerprint);
|
|
1195
|
+
if (alias?.scope_kind === 'user') {
|
|
1196
|
+
return alias.scope_key;
|
|
1197
|
+
}
|
|
1198
|
+
return paths.tokenFingerprint;
|
|
1199
|
+
}
|
|
1200
|
+
function resolveLinearBudgetLockPath(paths, lockScopeKey) {
|
|
1201
|
+
return join(paths.directory, `${lockScopeKey}.lock`);
|
|
1202
|
+
}
|
|
1203
|
+
async function readNewestPersistedLinearBudgetStatus(paths, scopeHint) {
|
|
1204
|
+
const alias = await readPersistedLinearBudgetAlias(paths.aliasPath, paths.tokenFingerprint);
|
|
1205
|
+
const candidatePaths = new Set([paths.legacyStatePath]);
|
|
1206
|
+
if (alias) {
|
|
1207
|
+
candidatePaths.add(resolveScopeStatePath(paths, alias.scope_key));
|
|
1208
|
+
}
|
|
1209
|
+
if (scopeHint?.viewer_id) {
|
|
1210
|
+
candidatePaths.add(resolveScopeStatePath(paths, resolveUserScopeKey(scopeHint.viewer_id, scopeHint.workspace_id)));
|
|
1211
|
+
}
|
|
1212
|
+
const candidates = [];
|
|
1213
|
+
for (const candidatePath of candidatePaths) {
|
|
1214
|
+
const persisted = await readPersistedLinearBudgetStatus(candidatePath, paths.tokenFingerprint);
|
|
1215
|
+
if (!persisted) {
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
candidates.push(persisted);
|
|
1219
|
+
}
|
|
1220
|
+
return mergePersistedLinearBudgetCandidates(candidates);
|
|
1221
|
+
}
|
|
1222
|
+
function mergePersistedLinearBudgetCandidates(candidates) {
|
|
1223
|
+
if (candidates.length === 0) {
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
const sorted = [...candidates].sort(comparePersistedLinearBudgetObservedAt);
|
|
1227
|
+
const preferredUserScope = [...sorted]
|
|
1228
|
+
.reverse()
|
|
1229
|
+
.find((candidate) => candidate.scope_kind === 'user' && candidate.viewer_id) ?? null;
|
|
1230
|
+
let merged = clonePersistedLinearBudgetStatus(sorted[0]);
|
|
1231
|
+
for (const candidate of sorted.slice(1)) {
|
|
1232
|
+
merged = mergePersistedLinearBudgetCandidate(merged, candidate);
|
|
1233
|
+
}
|
|
1234
|
+
if (!preferredUserScope) {
|
|
1235
|
+
return merged;
|
|
1236
|
+
}
|
|
1237
|
+
return {
|
|
1238
|
+
...merged,
|
|
1239
|
+
scope_kind: preferredUserScope.scope_kind,
|
|
1240
|
+
scope_key: preferredUserScope.scope_key,
|
|
1241
|
+
viewer_id: preferredUserScope.viewer_id,
|
|
1242
|
+
workspace_id: preferredUserScope.workspace_id
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
function comparePersistedLinearBudgetObservedAt(left, right) {
|
|
1246
|
+
const leftObservedAtMs = parseIsoToMs(left.observed_at) ?? Number.NEGATIVE_INFINITY;
|
|
1247
|
+
const rightObservedAtMs = parseIsoToMs(right.observed_at) ?? Number.NEGATIVE_INFINITY;
|
|
1248
|
+
return leftObservedAtMs - rightObservedAtMs;
|
|
1249
|
+
}
|
|
1250
|
+
function mergePersistedLinearBudgetCandidate(existing, candidate) {
|
|
1251
|
+
const endpoints = clonePersistedEndpoints(existing.endpoints);
|
|
1252
|
+
for (const [key, endpoint] of Object.entries(candidate.endpoints)) {
|
|
1253
|
+
upsertPersistedEndpointObservation(endpoints, {
|
|
1254
|
+
key,
|
|
1255
|
+
endpoint_name: endpoint.endpoint_name,
|
|
1256
|
+
aliases: [...endpoint.aliases],
|
|
1257
|
+
observed_at: endpoint.observed_at,
|
|
1258
|
+
requests: cloneBucket(endpoint.requests),
|
|
1259
|
+
complexity: cloneBucket(endpoint.complexity),
|
|
1260
|
+
request_complexity: endpoint.request_complexity,
|
|
1261
|
+
assume_unknown_resets_exhausted: false
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
const selectedEndpointKey = candidate.selected_endpoint_key ?? existing.selected_endpoint_key;
|
|
1265
|
+
const merged = {
|
|
1266
|
+
...clonePersistedLinearBudgetStatus(existing),
|
|
1267
|
+
observed_at: maxIsoTimestamp(existing.observed_at, candidate.observed_at) ?? candidate.observed_at,
|
|
1268
|
+
source: candidate.source,
|
|
1269
|
+
request_id: candidate.request_id ?? existing.request_id,
|
|
1270
|
+
retry_after_seconds: candidate.retry_after_seconds,
|
|
1271
|
+
cooldown_until: null,
|
|
1272
|
+
requests: mergeLinearBudgetBucket(existing.requests, candidate.requests),
|
|
1273
|
+
complexity: mergeLinearBudgetBucket(existing.complexity, candidate.complexity),
|
|
1274
|
+
request_complexity: candidate.request_complexity ?? existing.request_complexity,
|
|
1275
|
+
selected_endpoint_key: selectedEndpointKey && !endpoints[selectedEndpointKey] ? null : selectedEndpointKey,
|
|
1276
|
+
token_fingerprints: uniqueStrings([...existing.token_fingerprints, ...candidate.token_fingerprints]),
|
|
1277
|
+
endpoints,
|
|
1278
|
+
reservations: mergePersistedLinearBudgetReservations(existing, candidate),
|
|
1279
|
+
request_burn_history: mergePersistedLinearBudgetRequestBurnHistory(existing.request_burn_history, candidate.request_burn_history)
|
|
1280
|
+
};
|
|
1281
|
+
merged.cooldown_until = resolvePersistedLinearBudgetCooldownUntil({
|
|
1282
|
+
persisted: merged,
|
|
1283
|
+
observedAt: candidate.observed_at,
|
|
1284
|
+
retryAfterSeconds: candidate.retry_after_seconds
|
|
1285
|
+
});
|
|
1286
|
+
return merged;
|
|
1287
|
+
}
|
|
1288
|
+
async function readPersistedLinearBudgetStatus(statePath, tokenFingerprint) {
|
|
1289
|
+
try {
|
|
1290
|
+
const parsed = JSON.parse(await readFile(statePath, 'utf8'));
|
|
1291
|
+
return parsePersistedLinearBudgetStatus(parsed, tokenFingerprint);
|
|
1292
|
+
}
|
|
1293
|
+
catch (error) {
|
|
1294
|
+
if (error?.code === 'ENOENT') {
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function parsePersistedLinearBudgetStatus(value, tokenFingerprint) {
|
|
1301
|
+
if (!value || typeof value !== 'object') {
|
|
1302
|
+
return null;
|
|
1303
|
+
}
|
|
1304
|
+
const record = value;
|
|
1305
|
+
if (record.schema_version === LINEAR_BUDGET_STATE_SCHEMA_VERSION) {
|
|
1306
|
+
return parseSchemaV2LinearBudgetStatus(record, tokenFingerprint);
|
|
1307
|
+
}
|
|
1308
|
+
if (record.schema_version === 1) {
|
|
1309
|
+
return parseLegacySchemaV1LinearBudgetStatus(record, tokenFingerprint);
|
|
1310
|
+
}
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
function parseSchemaV2LinearBudgetStatus(record, tokenFingerprint) {
|
|
1314
|
+
const scopeKind = normalizeOptionalString(record.scope_kind);
|
|
1315
|
+
const scopeKey = normalizeOptionalString(record.scope_key);
|
|
1316
|
+
const observedAt = normalizeOptionalString(record.observed_at);
|
|
1317
|
+
const source = normalizeOptionalString(record.source);
|
|
1318
|
+
if ((scopeKind !== 'user' && scopeKind !== 'token') || !scopeKey || !observedAt || !source) {
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
const tokenFingerprints = Array.isArray(record.token_fingerprints)
|
|
1322
|
+
? record.token_fingerprints
|
|
1323
|
+
.map((entry) => normalizeOptionalString(entry))
|
|
1324
|
+
.filter((entry) => entry !== null)
|
|
1325
|
+
: [];
|
|
1326
|
+
if (tokenFingerprints.length === 0) {
|
|
1327
|
+
tokenFingerprints.push(tokenFingerprint);
|
|
1328
|
+
}
|
|
1329
|
+
const endpoints = record.endpoints && typeof record.endpoints === 'object'
|
|
1330
|
+
? Object.fromEntries(Object.entries(record.endpoints)
|
|
1331
|
+
.map(([key, value]) => [key, parsePersistedLinearBudgetEndpointStatus(value)])
|
|
1332
|
+
.filter((entry) => entry[1] !== null))
|
|
1333
|
+
: {};
|
|
1334
|
+
const reservations = Array.isArray(record.reservations)
|
|
1335
|
+
? record.reservations
|
|
1336
|
+
.map((entry) => parsePersistedLinearBudgetReservation(entry))
|
|
1337
|
+
.filter((entry) => entry !== null)
|
|
1338
|
+
: [];
|
|
1339
|
+
const requestBurnHistory = Array.isArray(record.request_burn_history)
|
|
1340
|
+
? record.request_burn_history
|
|
1341
|
+
.map((entry) => parsePersistedLinearBudgetRequestBurnHistoryEntry(entry))
|
|
1342
|
+
.filter((entry) => entry !== null)
|
|
1343
|
+
: [];
|
|
1344
|
+
return {
|
|
1345
|
+
schema_version: 2,
|
|
1346
|
+
scope_kind: scopeKind,
|
|
1347
|
+
scope_key: scopeKey,
|
|
1348
|
+
viewer_id: normalizeOptionalString(record.viewer_id),
|
|
1349
|
+
workspace_id: normalizeOptionalString(record.workspace_id),
|
|
1350
|
+
token_fingerprints: uniqueStrings(tokenFingerprints),
|
|
1351
|
+
observed_at: observedAt,
|
|
1352
|
+
source,
|
|
1353
|
+
request_id: normalizeOptionalString(record.request_id),
|
|
1354
|
+
retry_after_seconds: parseNumberLike(record.retry_after_seconds),
|
|
1355
|
+
cooldown_until: normalizeOptionalString(record.cooldown_until),
|
|
1356
|
+
requests: parsePersistedLinearBudgetBucket(record.requests),
|
|
1357
|
+
complexity: parsePersistedLinearBudgetBucket(record.complexity),
|
|
1358
|
+
request_complexity: parseNumberLike(record.request_complexity),
|
|
1359
|
+
selected_endpoint_key: normalizeOptionalString(record.selected_endpoint_key),
|
|
1360
|
+
endpoints,
|
|
1361
|
+
reservations,
|
|
1362
|
+
request_burn_history: requestBurnHistory
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
function parseLegacySchemaV1LinearBudgetStatus(record, tokenFingerprint) {
|
|
1366
|
+
if (normalizeOptionalString(record.token_fingerprint) !== tokenFingerprint) {
|
|
1367
|
+
return null;
|
|
1368
|
+
}
|
|
1369
|
+
const observedAt = normalizeOptionalString(record.observed_at);
|
|
1370
|
+
const source = normalizeOptionalString(record.source);
|
|
1371
|
+
if (!observedAt || !source) {
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
const legacyEndpointRequests = parsePersistedLinearBudgetBucket(record.endpoint_requests);
|
|
1375
|
+
const legacyEndpointComplexity = parsePersistedLinearBudgetBucket(record.endpoint_complexity);
|
|
1376
|
+
const selectedEndpointKey = legacyEndpointRequests || legacyEndpointComplexity ? buildEndpointKey(null, source) : null;
|
|
1377
|
+
return {
|
|
1378
|
+
schema_version: 2,
|
|
1379
|
+
scope_kind: 'token',
|
|
1380
|
+
scope_key: tokenFingerprint,
|
|
1381
|
+
viewer_id: null,
|
|
1382
|
+
workspace_id: null,
|
|
1383
|
+
token_fingerprints: [tokenFingerprint],
|
|
1384
|
+
observed_at: observedAt,
|
|
1385
|
+
source,
|
|
1386
|
+
request_id: normalizeOptionalString(record.request_id),
|
|
1387
|
+
retry_after_seconds: parseNumberLike(record.retry_after_seconds),
|
|
1388
|
+
cooldown_until: normalizeOptionalString(record.cooldown_until),
|
|
1389
|
+
requests: parsePersistedLinearBudgetBucket(record.requests),
|
|
1390
|
+
complexity: parsePersistedLinearBudgetBucket(record.complexity),
|
|
1391
|
+
request_complexity: null,
|
|
1392
|
+
selected_endpoint_key: selectedEndpointKey,
|
|
1393
|
+
endpoints: selectedEndpointKey === null
|
|
1394
|
+
? {}
|
|
1395
|
+
: {
|
|
1396
|
+
[selectedEndpointKey]: {
|
|
1397
|
+
endpoint_name: null,
|
|
1398
|
+
aliases: [source],
|
|
1399
|
+
observed_at: observedAt,
|
|
1400
|
+
requests: legacyEndpointRequests,
|
|
1401
|
+
complexity: legacyEndpointComplexity,
|
|
1402
|
+
request_complexity: null
|
|
1403
|
+
}
|
|
1404
|
+
},
|
|
1405
|
+
reservations: [],
|
|
1406
|
+
request_burn_history: []
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
function parsePersistedLinearBudgetEndpointStatus(value) {
|
|
1410
|
+
if (!value || typeof value !== 'object') {
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
const record = value;
|
|
1414
|
+
const observedAt = normalizeOptionalString(record.observed_at);
|
|
1415
|
+
if (!observedAt) {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
const aliases = Array.isArray(record.aliases)
|
|
1419
|
+
? record.aliases
|
|
1420
|
+
.map((entry) => normalizeOptionalString(entry))
|
|
1421
|
+
.filter((entry) => entry !== null)
|
|
1422
|
+
: [];
|
|
1423
|
+
return {
|
|
1424
|
+
endpoint_name: normalizeOptionalString(record.endpoint_name),
|
|
1425
|
+
aliases: uniqueStrings(aliases),
|
|
1426
|
+
observed_at: observedAt,
|
|
1427
|
+
requests: parsePersistedLinearBudgetBucket(record.requests),
|
|
1428
|
+
complexity: parsePersistedLinearBudgetBucket(record.complexity),
|
|
1429
|
+
request_complexity: parseNumberLike(record.request_complexity)
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
function parsePersistedLinearBudgetReservation(value) {
|
|
1433
|
+
if (!value || typeof value !== 'object') {
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
const record = value;
|
|
1437
|
+
const id = normalizeRequiredString(record.id);
|
|
1438
|
+
const operation = normalizeRequiredString(record.operation);
|
|
1439
|
+
const createdAt = normalizeOptionalString(record.created_at);
|
|
1440
|
+
const expiresAt = normalizeOptionalString(record.expires_at);
|
|
1441
|
+
const requests = normalizePositiveInteger(record.requests);
|
|
1442
|
+
const complexity = normalizePositiveInteger(record.complexity);
|
|
1443
|
+
if (!id || !operation || !createdAt || !expiresAt || requests === null) {
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
return {
|
|
1447
|
+
id,
|
|
1448
|
+
operation,
|
|
1449
|
+
endpoint_key: normalizeOptionalString(record.endpoint_key),
|
|
1450
|
+
requests,
|
|
1451
|
+
complexity,
|
|
1452
|
+
created_at: createdAt,
|
|
1453
|
+
expires_at: expiresAt
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
function parsePersistedLinearBudgetRequestBurnHistoryEntry(value) {
|
|
1457
|
+
if (!value || typeof value !== 'object') {
|
|
1458
|
+
return null;
|
|
1459
|
+
}
|
|
1460
|
+
const record = value;
|
|
1461
|
+
const recordedAt = normalizeOptionalString(record.recorded_at);
|
|
1462
|
+
const observedAt = normalizeOptionalString(record.observed_at);
|
|
1463
|
+
const source = normalizeRequiredString(record.source);
|
|
1464
|
+
const requestBucket = normalizeOptionalString(record.request_bucket);
|
|
1465
|
+
if (!recordedAt || !observedAt || !source) {
|
|
1466
|
+
return null;
|
|
1467
|
+
}
|
|
1468
|
+
const operation = normalizeRequiredString(record.operation) ?? source;
|
|
1469
|
+
return {
|
|
1470
|
+
recorded_at: recordedAt,
|
|
1471
|
+
observed_at: observedAt,
|
|
1472
|
+
source,
|
|
1473
|
+
operation,
|
|
1474
|
+
run_id: normalizeOptionalString(record.run_id),
|
|
1475
|
+
process_pid: parseNumberLike(record.process_pid),
|
|
1476
|
+
process_title: normalizeOptionalString(record.process_title),
|
|
1477
|
+
request_id: normalizeOptionalString(record.request_id),
|
|
1478
|
+
request_bucket: requestBucket === 'requests' || requestBucket === 'endpoint_requests'
|
|
1479
|
+
? requestBucket
|
|
1480
|
+
: null,
|
|
1481
|
+
remaining: parseNumberLike(record.remaining),
|
|
1482
|
+
remaining_delta: parseSignedInteger(record.remaining_delta),
|
|
1483
|
+
reset_at: normalizeOptionalString(record.reset_at),
|
|
1484
|
+
suppression_reason: normalizeOptionalString(record.suppression_reason),
|
|
1485
|
+
cooldown_reason: normalizeOptionalString(record.cooldown_reason)
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
function parsePersistedLinearBudgetBucket(value) {
|
|
1489
|
+
if (!value || typeof value !== 'object') {
|
|
1490
|
+
return null;
|
|
1491
|
+
}
|
|
1492
|
+
const record = value;
|
|
1493
|
+
const limit = parseNumberLike(record.limit);
|
|
1494
|
+
const remaining = parseNumberLike(record.remaining);
|
|
1495
|
+
const resetAt = normalizeOptionalString(record.reset_at);
|
|
1496
|
+
if (limit === null && remaining === null && resetAt === null) {
|
|
1497
|
+
return null;
|
|
1498
|
+
}
|
|
1499
|
+
return {
|
|
1500
|
+
limit,
|
|
1501
|
+
remaining,
|
|
1502
|
+
reset_at: resetAt
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
async function writePersistedLinearBudgetStatus(paths, persisted) {
|
|
1506
|
+
const statePath = resolvePersistedStatePath(paths, persisted);
|
|
1507
|
+
await mkdir(dirname(statePath), { recursive: true });
|
|
1508
|
+
await writeJsonAtomic(statePath, persisted);
|
|
1509
|
+
}
|
|
1510
|
+
async function writePersistedLinearBudgetAlias(paths, scope) {
|
|
1511
|
+
const aliasRecord = {
|
|
1512
|
+
schema_version: LINEAR_BUDGET_ALIAS_SCHEMA_VERSION,
|
|
1513
|
+
token_fingerprint: paths.tokenFingerprint,
|
|
1514
|
+
scope_kind: scope.kind,
|
|
1515
|
+
scope_key: scope.key,
|
|
1516
|
+
viewer_id: scope.viewer_id,
|
|
1517
|
+
workspace_id: scope.workspace_id,
|
|
1518
|
+
updated_at: new Date().toISOString()
|
|
1519
|
+
};
|
|
1520
|
+
await mkdir(dirname(paths.aliasPath), { recursive: true });
|
|
1521
|
+
await writeJsonAtomic(paths.aliasPath, aliasRecord);
|
|
1522
|
+
}
|
|
1523
|
+
async function readPersistedLinearBudgetAlias(aliasPath, tokenFingerprint) {
|
|
1524
|
+
try {
|
|
1525
|
+
const parsed = JSON.parse(await readFile(aliasPath, 'utf8'));
|
|
1526
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
const record = parsed;
|
|
1530
|
+
if (record.schema_version !== LINEAR_BUDGET_ALIAS_SCHEMA_VERSION) {
|
|
1531
|
+
return null;
|
|
1532
|
+
}
|
|
1533
|
+
if (normalizeOptionalString(record.token_fingerprint) !== tokenFingerprint) {
|
|
1534
|
+
return null;
|
|
1535
|
+
}
|
|
1536
|
+
const scopeKind = normalizeOptionalString(record.scope_kind);
|
|
1537
|
+
const scopeKey = normalizeOptionalString(record.scope_key);
|
|
1538
|
+
const updatedAt = normalizeOptionalString(record.updated_at);
|
|
1539
|
+
if ((scopeKind !== 'user' && scopeKind !== 'token') || !scopeKey || !updatedAt) {
|
|
1540
|
+
return null;
|
|
1541
|
+
}
|
|
1542
|
+
return {
|
|
1543
|
+
schema_version: 1,
|
|
1544
|
+
token_fingerprint: tokenFingerprint,
|
|
1545
|
+
scope_kind: scopeKind,
|
|
1546
|
+
scope_key: scopeKey,
|
|
1547
|
+
viewer_id: normalizeOptionalString(record.viewer_id),
|
|
1548
|
+
workspace_id: normalizeOptionalString(record.workspace_id),
|
|
1549
|
+
updated_at: updatedAt
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
catch (error) {
|
|
1553
|
+
if (error?.code === 'ENOENT') {
|
|
1554
|
+
return null;
|
|
1555
|
+
}
|
|
1556
|
+
return null;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
async function resolveWriteScope(paths, scopeHint, existing) {
|
|
1560
|
+
const normalizedHint = normalizeScopeHint(scopeHint);
|
|
1561
|
+
if (normalizedHint?.viewer_id) {
|
|
1562
|
+
return {
|
|
1563
|
+
kind: 'user',
|
|
1564
|
+
key: resolveUserScopeKey(normalizedHint.viewer_id, normalizedHint.workspace_id),
|
|
1565
|
+
viewer_id: normalizedHint.viewer_id,
|
|
1566
|
+
workspace_id: normalizedHint.workspace_id,
|
|
1567
|
+
token_fingerprint: paths.tokenFingerprint
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
if (existing?.scope_kind === 'user' && existing.viewer_id) {
|
|
1571
|
+
return {
|
|
1572
|
+
kind: 'user',
|
|
1573
|
+
key: existing.scope_key,
|
|
1574
|
+
viewer_id: existing.viewer_id,
|
|
1575
|
+
workspace_id: existing.workspace_id,
|
|
1576
|
+
token_fingerprint: paths.tokenFingerprint
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
const alias = await readPersistedLinearBudgetAlias(paths.aliasPath, paths.tokenFingerprint);
|
|
1580
|
+
if (alias?.scope_kind === 'user' && alias.viewer_id) {
|
|
1581
|
+
return {
|
|
1582
|
+
kind: 'user',
|
|
1583
|
+
key: alias.scope_key,
|
|
1584
|
+
viewer_id: alias.viewer_id,
|
|
1585
|
+
workspace_id: alias.workspace_id,
|
|
1586
|
+
token_fingerprint: paths.tokenFingerprint
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
return {
|
|
1590
|
+
kind: 'token',
|
|
1591
|
+
key: paths.tokenFingerprint,
|
|
1592
|
+
viewer_id: null,
|
|
1593
|
+
workspace_id: normalizedHint?.workspace_id ?? existing?.workspace_id ?? alias?.workspace_id ?? null,
|
|
1594
|
+
token_fingerprint: paths.tokenFingerprint
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
function normalizeScopeHint(value) {
|
|
1598
|
+
if (!value) {
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
const viewerId = normalizeOptionalString(value.viewerId);
|
|
1602
|
+
const workspaceId = normalizeOptionalString(value.workspaceId);
|
|
1603
|
+
if (!viewerId && !workspaceId) {
|
|
1604
|
+
return null;
|
|
1605
|
+
}
|
|
1606
|
+
return {
|
|
1607
|
+
viewer_id: viewerId,
|
|
1608
|
+
workspace_id: workspaceId
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
function resolvePersistedStatePath(paths, persisted) {
|
|
1612
|
+
return persisted.scope_kind === 'user'
|
|
1613
|
+
? resolveScopeStatePath(paths, persisted.scope_key)
|
|
1614
|
+
: paths.legacyStatePath;
|
|
1615
|
+
}
|
|
1616
|
+
function resolveScopeStatePath(paths, scopeKey) {
|
|
1617
|
+
return join(paths.scopesDir, `${scopeKey}.json`);
|
|
1618
|
+
}
|
|
1619
|
+
function resolveUserScopeKey(viewerId, workspaceId) {
|
|
1620
|
+
return createHash('sha256')
|
|
1621
|
+
.update(`linear-user:${workspaceId ?? 'unknown-workspace'}:${viewerId}`)
|
|
1622
|
+
.digest('hex');
|
|
1623
|
+
}
|
|
1624
|
+
function buildEndpointKey(endpointName, source) {
|
|
1625
|
+
if (endpointName) {
|
|
1626
|
+
return `endpoint:${normalizeKeyFragment(endpointName)}`;
|
|
1627
|
+
}
|
|
1628
|
+
return `source:${normalizeKeyFragment(source)}`;
|
|
1629
|
+
}
|
|
1630
|
+
function normalizeKeyFragment(value) {
|
|
1631
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/gu, '-').replace(/^-+|-+$/gu, '') || 'unknown';
|
|
1632
|
+
}
|
|
1633
|
+
function cloneBucket(bucket) {
|
|
1634
|
+
return bucket
|
|
1635
|
+
? {
|
|
1636
|
+
limit: bucket.limit,
|
|
1637
|
+
remaining: bucket.remaining,
|
|
1638
|
+
reset_at: bucket.reset_at
|
|
1639
|
+
}
|
|
1640
|
+
: null;
|
|
1641
|
+
}
|
|
1642
|
+
function mergeLinearBudgetBucket(existing, observation) {
|
|
1643
|
+
if (!existing) {
|
|
1644
|
+
return cloneBucket(observation);
|
|
1645
|
+
}
|
|
1646
|
+
if (!observation) {
|
|
1647
|
+
return cloneBucket(existing);
|
|
1648
|
+
}
|
|
1649
|
+
return {
|
|
1650
|
+
limit: observation.limit ?? existing.limit,
|
|
1651
|
+
remaining: observation.remaining ?? existing.remaining,
|
|
1652
|
+
reset_at: observation.reset_at ?? existing.reset_at
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
function subtractReservationFromBucket(bucket, reservedAmount) {
|
|
1656
|
+
if (!bucket || bucket.remaining === null || reservedAmount <= 0) {
|
|
1657
|
+
return bucket;
|
|
1658
|
+
}
|
|
1659
|
+
return {
|
|
1660
|
+
limit: bucket.limit,
|
|
1661
|
+
remaining: bucket.remaining - reservedAmount,
|
|
1662
|
+
reset_at: bucket.reset_at
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
function normalizeExpiredLinearBudgetBucket(bucket, observedAt, options = {}) {
|
|
1666
|
+
if (!bucket) {
|
|
1667
|
+
return null;
|
|
1668
|
+
}
|
|
1669
|
+
const resetAtMs = parseIsoToMs(bucket.reset_at);
|
|
1670
|
+
if (resetAtMs === null) {
|
|
1671
|
+
const observedAtMs = parseIsoToMs(observedAt);
|
|
1672
|
+
if (observedAtMs !== null &&
|
|
1673
|
+
bucket.remaining !== null &&
|
|
1674
|
+
bucket.remaining <= 0 &&
|
|
1675
|
+
observedAtMs + LINEAR_BUDGET_UNKNOWN_RESET_EXHAUSTED_GRACE_MS <= Date.now()) {
|
|
1676
|
+
if (bucket.limit === null) {
|
|
1677
|
+
return null;
|
|
1678
|
+
}
|
|
1679
|
+
return {
|
|
1680
|
+
limit: bucket.limit,
|
|
1681
|
+
remaining: null,
|
|
1682
|
+
reset_at: null
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
const requestReserveBucket = options.requestReserveBucket;
|
|
1686
|
+
if (requestReserveBucket) {
|
|
1687
|
+
const reserve = resolveRequestPollingHeadroomReserve(bucket.limit, requestReserveBucket === 'endpoint_requests');
|
|
1688
|
+
if (observedAtMs !== null &&
|
|
1689
|
+
bucket.remaining !== null &&
|
|
1690
|
+
bucket.remaining <= reserve &&
|
|
1691
|
+
observedAtMs + LINEAR_BUDGET_UNKNOWN_RESET_EXHAUSTED_GRACE_MS <= Date.now()) {
|
|
1692
|
+
return {
|
|
1693
|
+
limit: bucket.limit,
|
|
1694
|
+
remaining: null,
|
|
1695
|
+
reset_at: null
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
return bucket;
|
|
1700
|
+
}
|
|
1701
|
+
if (resetAtMs > Date.now()) {
|
|
1702
|
+
return bucket;
|
|
1703
|
+
}
|
|
1704
|
+
if (bucket.limit === null) {
|
|
1705
|
+
return null;
|
|
1706
|
+
}
|
|
1707
|
+
return {
|
|
1708
|
+
limit: bucket.limit,
|
|
1709
|
+
remaining: null,
|
|
1710
|
+
reset_at: null
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
function pruneExpiredReservations(persisted) {
|
|
1714
|
+
const reservations = persisted.reservations.filter((entry) => isFutureIsoTimestamp(entry.expires_at));
|
|
1715
|
+
return reservations.length === persisted.reservations.length
|
|
1716
|
+
? persisted
|
|
1717
|
+
: {
|
|
1718
|
+
...persisted,
|
|
1719
|
+
reservations
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
function applyDeterministicPositiveJitter(baseIntervalMs, seed, nowMs) {
|
|
1723
|
+
if (baseIntervalMs <= 0) {
|
|
1724
|
+
return baseIntervalMs;
|
|
1725
|
+
}
|
|
1726
|
+
const maxJitterMs = Math.min(LINEAR_BUDGET_POLL_JITTER_MAX_MS, Math.max(1_000, Math.floor(baseIntervalMs * LINEAR_BUDGET_POLL_JITTER_RATIO)));
|
|
1727
|
+
const timeBucket = Math.floor(nowMs / Math.max(baseIntervalMs, 1));
|
|
1728
|
+
const hash = stableHash(`${seed}|${timeBucket}`);
|
|
1729
|
+
return baseIntervalMs + (hash % (maxJitterMs + 1));
|
|
1730
|
+
}
|
|
1731
|
+
function stableHash(value) {
|
|
1732
|
+
let hash = 0;
|
|
1733
|
+
for (const character of value) {
|
|
1734
|
+
hash = (hash * 31 + character.charCodeAt(0)) >>> 0;
|
|
1735
|
+
}
|
|
1736
|
+
return hash;
|
|
1737
|
+
}
|
|
1738
|
+
function maxIsoTimestamp(left, right) {
|
|
1739
|
+
const leftMs = parseIsoToMs(left);
|
|
1740
|
+
const rightMs = parseIsoToMs(right);
|
|
1741
|
+
if (leftMs === null) {
|
|
1742
|
+
return right;
|
|
1743
|
+
}
|
|
1744
|
+
if (rightMs === null) {
|
|
1745
|
+
return left;
|
|
1746
|
+
}
|
|
1747
|
+
return rightMs >= leftMs ? right : left;
|
|
1748
|
+
}
|
|
1749
|
+
function uniqueStrings(values) {
|
|
1750
|
+
return [...new Set(values.filter((value) => value.trim().length > 0))];
|
|
1751
|
+
}
|
|
1752
|
+
function normalizePositiveInteger(value) {
|
|
1753
|
+
const parsed = parseNumberLike(value);
|
|
1754
|
+
return parsed !== null && parsed > 0 ? parsed : null;
|
|
1755
|
+
}
|
|
1756
|
+
function parseSignedInteger(value) {
|
|
1757
|
+
return parseNumberLike(value);
|
|
1758
|
+
}
|
|
1759
|
+
function parseNumberLike(value) {
|
|
1760
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
1761
|
+
return Math.trunc(value);
|
|
1762
|
+
}
|
|
1763
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
1764
|
+
return null;
|
|
1765
|
+
}
|
|
1766
|
+
const parsed = Number.parseInt(value, 10);
|
|
1767
|
+
return Number.isInteger(parsed) ? parsed : null;
|
|
1768
|
+
}
|
|
1769
|
+
function parseIsoToMs(value) {
|
|
1770
|
+
const normalized = normalizeOptionalString(value);
|
|
1771
|
+
if (!normalized) {
|
|
1772
|
+
return null;
|
|
1773
|
+
}
|
|
1774
|
+
const parsed = Date.parse(normalized);
|
|
1775
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1776
|
+
}
|
|
1777
|
+
function isFutureIsoTimestamp(value) {
|
|
1778
|
+
const parsed = parseIsoToMs(value);
|
|
1779
|
+
return parsed !== null && parsed > Date.now();
|
|
1780
|
+
}
|
|
1781
|
+
function normalizeOptionalString(value) {
|
|
1782
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
1783
|
+
}
|
|
1784
|
+
function normalizeRequiredString(value) {
|
|
1785
|
+
return normalizeOptionalString(value);
|
|
1786
|
+
}
|
|
1787
|
+
function hasRecordableLinearBudgetDetails(details) {
|
|
1788
|
+
return Object.keys(details).some((key) => key !== 'request_id' && key !== 'errors');
|
|
1789
|
+
}
|