open-multi-agent-kit 0.96.1 → 0.97.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/README.md +14 -2
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/commands/resource-doctor-cli.d.ts +22 -0
- package/dist/commands/resource-doctor-cli.d.ts.map +1 -0
- package/dist/commands/resource-doctor-cli.js +98 -0
- package/dist/commands/resource-doctor-cli.js.map +1 -0
- package/dist/core/agent-session.d.ts +46 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +373 -12
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/attachment-store.d.ts +39 -0
- package/dist/core/attachment-store.d.ts.map +1 -0
- package/dist/core/attachment-store.js +81 -0
- package/dist/core/attachment-store.js.map +1 -0
- package/dist/core/command-safety.d.ts +8 -0
- package/dist/core/command-safety.d.ts.map +1 -1
- package/dist/core/command-safety.js +140 -14
- package/dist/core/command-safety.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +6 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +35 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/completion-sound.d.ts +109 -0
- package/dist/core/completion-sound.d.ts.map +1 -0
- package/dist/core/completion-sound.js +165 -0
- package/dist/core/completion-sound.js.map +1 -0
- package/dist/core/context-budget-governor-v2.d.ts +1 -1
- package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
- package/dist/core/context-budget-governor-v2.js +1 -1
- package/dist/core/context-budget-governor-v2.js.map +1 -1
- package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
- package/dist/core/context-budget-system-prompt-items.js +2 -2
- package/dist/core/context-budget-system-prompt-items.js.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.js +2 -0
- package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.js +3 -0
- package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +2 -1
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-scoring.d.ts +17 -0
- package/dist/core/context-budget-v2-scoring.d.ts.map +1 -1
- package/dist/core/context-budget-v2-scoring.js +25 -8
- package/dist/core/context-budget-v2-scoring.js.map +1 -1
- package/dist/core/context-budget-v2-selection.d.ts +2 -2
- package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
- package/dist/core/context-budget-v2-selection.js +18 -1
- package/dist/core/context-budget-v2-selection.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +17 -0
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js +15 -0
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/extensions/builtin/command-safety-gate.d.ts +3 -1
- package/dist/core/extensions/builtin/command-safety-gate.d.ts.map +1 -1
- package/dist/core/extensions/builtin/command-safety-gate.js +7 -0
- package/dist/core/extensions/builtin/command-safety-gate.js.map +1 -1
- package/dist/core/extensions/types.d.ts +12 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/host-resource-probe.d.ts +63 -0
- package/dist/core/host-resource-probe.d.ts.map +1 -0
- package/dist/core/host-resource-probe.js +126 -0
- package/dist/core/host-resource-probe.js.map +1 -0
- package/dist/core/host-resource-snapshot.d.ts +54 -0
- package/dist/core/host-resource-snapshot.d.ts.map +1 -0
- package/dist/core/host-resource-snapshot.js +139 -0
- package/dist/core/host-resource-snapshot.js.map +1 -0
- package/dist/core/model-registry.d.ts +8 -3
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +71 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/package-doctor-source-scan.d.ts +9 -0
- package/dist/core/package-doctor-source-scan.d.ts.map +1 -1
- package/dist/core/package-doctor-source-scan.js +77 -4
- package/dist/core/package-doctor-source-scan.js.map +1 -1
- package/dist/core/prompt-attachment.d.ts +51 -0
- package/dist/core/prompt-attachment.d.ts.map +1 -0
- package/dist/core/prompt-attachment.js +186 -0
- package/dist/core/prompt-attachment.js.map +1 -0
- package/dist/core/prompt-settlement.d.ts +81 -0
- package/dist/core/prompt-settlement.d.ts.map +1 -0
- package/dist/core/prompt-settlement.js +84 -0
- package/dist/core/prompt-settlement.js.map +1 -0
- package/dist/core/provider-resilience.d.ts +28 -0
- package/dist/core/provider-resilience.d.ts.map +1 -1
- package/dist/core/provider-resilience.js +51 -1
- package/dist/core/provider-resilience.js.map +1 -1
- package/dist/core/provider-retry.d.ts +7 -0
- package/dist/core/provider-retry.d.ts.map +1 -1
- package/dist/core/provider-retry.js +24 -0
- package/dist/core/provider-retry.js.map +1 -1
- package/dist/core/provider-usage.d.ts +15 -2
- package/dist/core/provider-usage.d.ts.map +1 -1
- package/dist/core/provider-usage.js +176 -2
- package/dist/core/provider-usage.js.map +1 -1
- package/dist/core/reasoning-router-promotion.d.ts +84 -0
- package/dist/core/reasoning-router-promotion.d.ts.map +1 -0
- package/dist/core/reasoning-router-promotion.js +92 -0
- package/dist/core/reasoning-router-promotion.js.map +1 -0
- package/dist/core/reasoning-router-resolver.d.ts.map +1 -1
- package/dist/core/reasoning-router-resolver.js +1 -1
- package/dist/core/reasoning-router-resolver.js.map +1 -1
- package/dist/core/resource-admission-config.d.ts +55 -0
- package/dist/core/resource-admission-config.d.ts.map +1 -0
- package/dist/core/resource-admission-config.js +97 -0
- package/dist/core/resource-admission-config.js.map +1 -0
- package/dist/core/resource-admission.d.ts +81 -0
- package/dist/core/resource-admission.d.ts.map +1 -0
- package/dist/core/resource-admission.js +128 -0
- package/dist/core/resource-admission.js.map +1 -0
- package/dist/core/resource-governor-format.d.ts +50 -0
- package/dist/core/resource-governor-format.d.ts.map +1 -0
- package/dist/core/resource-governor-format.js +113 -0
- package/dist/core/resource-governor-format.js.map +1 -0
- package/dist/core/resource-governor-settings.d.ts +56 -0
- package/dist/core/resource-governor-settings.d.ts.map +1 -0
- package/dist/core/resource-governor-settings.js +89 -0
- package/dist/core/resource-governor-settings.js.map +1 -0
- package/dist/core/resource-observation-journal.d.ts +74 -0
- package/dist/core/resource-observation-journal.d.ts.map +1 -0
- package/dist/core/resource-observation-journal.js +198 -0
- package/dist/core/resource-observation-journal.js.map +1 -0
- package/dist/core/resource-safety-gate.d.ts +54 -0
- package/dist/core/resource-safety-gate.d.ts.map +1 -0
- package/dist/core/resource-safety-gate.js +51 -0
- package/dist/core/resource-safety-gate.js.map +1 -0
- package/dist/core/run-journal.d.ts.map +1 -1
- package/dist/core/run-journal.js +13 -1
- package/dist/core/run-journal.js.map +1 -1
- package/dist/core/run-resource-lease.d.ts +73 -0
- package/dist/core/run-resource-lease.d.ts.map +1 -0
- package/dist/core/run-resource-lease.js +78 -0
- package/dist/core/run-resource-lease.js.map +1 -0
- package/dist/core/session-bash-service.d.ts +12 -0
- package/dist/core/session-bash-service.d.ts.map +1 -1
- package/dist/core/session-bash-service.js +18 -3
- package/dist/core/session-bash-service.js.map +1 -1
- package/dist/core/session-system-prompt.d.ts +2 -0
- package/dist/core/session-system-prompt.d.ts.map +1 -1
- package/dist/core/session-system-prompt.js +2 -0
- package/dist/core/session-system-prompt.js.map +1 -1
- package/dist/core/session-termination.d.ts +8 -3
- package/dist/core/session-termination.d.ts.map +1 -1
- package/dist/core/session-termination.js +40 -3
- package/dist/core/session-termination.js.map +1 -1
- package/dist/core/settings-manager.d.ts +12 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +36 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/subagent-lane-launcher.d.ts +64 -0
- package/dist/core/subagent-lane-launcher.d.ts.map +1 -0
- package/dist/core/subagent-lane-launcher.js +107 -0
- package/dist/core/subagent-lane-launcher.js.map +1 -0
- package/dist/core/system-cpu-sampler.d.ts +45 -0
- package/dist/core/system-cpu-sampler.d.ts.map +1 -0
- package/dist/core/system-cpu-sampler.js +95 -0
- package/dist/core/system-cpu-sampler.js.map +1 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +5 -8
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +3 -7
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +13 -48
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +1 -34
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/workload-classifier.d.ts +32 -0
- package/dist/core/workload-classifier.d.ts.map +1 -0
- package/dist/core/workload-classifier.js +66 -0
- package/dist/core/workload-classifier.js.map +1 -0
- package/dist/core/workload-family-matcher.d.ts +3 -0
- package/dist/core/workload-family-matcher.d.ts.map +1 -0
- package/dist/core/workload-family-matcher.js +126 -0
- package/dist/core/workload-family-matcher.js.map +1 -0
- package/dist/core/workload-permit-pool.d.ts +78 -0
- package/dist/core/workload-permit-pool.d.ts.map +1 -0
- package/dist/core/workload-permit-pool.js +138 -0
- package/dist/core/workload-permit-pool.js.map +1 -0
- package/dist/core/workload-shard-executor.d.ts +68 -0
- package/dist/core/workload-shard-executor.d.ts.map +1 -0
- package/dist/core/workload-shard-executor.js +156 -0
- package/dist/core/workload-shard-executor.js.map +1 -0
- package/dist/core/workload-shard-plan.d.ts +81 -0
- package/dist/core/workload-shard-plan.d.ts.map +1 -0
- package/dist/core/workload-shard-plan.js +183 -0
- package/dist/core/workload-shard-plan.js.map +1 -0
- package/dist/core/workload-shard-runner.d.ts +11 -0
- package/dist/core/workload-shard-runner.d.ts.map +1 -0
- package/dist/core/workload-shard-runner.js +98 -0
- package/dist/core/workload-shard-runner.js.map +1 -0
- package/dist/core/workload-shard-store.d.ts +44 -0
- package/dist/core/workload-shard-store.d.ts.map +1 -0
- package/dist/core/workload-shard-store.js +189 -0
- package/dist/core/workload-shard-store.js.map +1 -0
- package/dist/core/workload-sharders.d.ts +47 -0
- package/dist/core/workload-sharders.d.ts.map +1 -0
- package/dist/core/workload-sharders.js +162 -0
- package/dist/core/workload-sharders.js.map +1 -0
- package/dist/core/workload-shell-scan.d.ts +12 -0
- package/dist/core/workload-shell-scan.d.ts.map +1 -0
- package/dist/core/workload-shell-scan.js +79 -0
- package/dist/core/workload-shell-scan.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +6 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/attachment-strip.d.ts +21 -0
- package/dist/modes/interactive/components/attachment-strip.d.ts.map +1 -0
- package/dist/modes/interactive/components/attachment-strip.js +136 -0
- package/dist/modes/interactive/components/attachment-strip.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts +6 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +26 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -1
- package/dist/modes/interactive/components/status-sidebar.js +12 -1
- package/dist/modes/interactive/components/status-sidebar.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +27 -4
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +255 -53
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/compaction.md +1 -1
- package/docs/extensions.md +12 -3
- package/docs/index.md +4 -1
- package/docs/metrics.md +74 -2
- package/docs/provider-resilience.md +11 -0
- package/docs/providers.md +1 -1
- package/docs/sdk.md +4 -0
- package/docs/settings.md +64 -2
- package/docs/skills.md +16 -0
- package/docs/usage.md +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/agent-capability-router.test.ts +7 -0
- package/examples/extensions/subagent/domain-profiles.ts +0 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +8 -10
- package/dist/core/tools/omp-pure-seams.d.ts +0 -41
- package/dist/core/tools/omp-pure-seams.d.ts.map +0 -1
- package/dist/core/tools/omp-pure-seams.js +0 -142
- package/dist/core/tools/omp-pure-seams.js.map +0 -1
- package/dist/core/tools/omp-seam-runtime.d.ts +0 -100
- package/dist/core/tools/omp-seam-runtime.d.ts.map +0 -1
- package/dist/core/tools/omp-seam-runtime.js +0 -40
- package/dist/core/tools/omp-seam-runtime.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workload-family-matcher.d.ts","sourceRoot":"","sources":["../../src/core/workload-family-matcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,sBAAsB,EAAyB,MAAM,0BAA0B,CAAC;AA4C7G,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,sBAAsB,CAQnF","sourcesContent":["import type { WorkloadClass, WorkloadClassification, WorkloadCommandFamily } from \"./workload-classifier.ts\";\n\nconst LIGHT_EXECUTABLES = new Set([\n\t\"ls\",\n\t\"cat\",\n\t\"head\",\n\t\"tail\",\n\t\"grep\",\n\t\"rg\",\n\t\"find\",\n\t\"echo\",\n\t\"pwd\",\n\t\"which\",\n\t\"wc\",\n\t\"stat\",\n\t\"file\",\n\t\"true\",\n\t\"false\",\n]);\nconst LIGHT_GIT_SUBCOMMANDS = new Set([\"status\", \"log\", \"diff\", \"show\", \"branch\", \"rev-parse\", \"blame\"]);\nconst IO_EXECUTABLES = new Set([\"cp\", \"mv\", \"rsync\", \"mkdir\", \"touch\", \"dd\"]);\nconst CPU_EXECUTABLES = new Set([\"eslint\", \"prettier\", \"biome\", \"ruff\", \"gofmt\", \"rustfmt\"]);\nconst PACKAGE_MANAGERS = new Set([\"npm\", \"pnpm\", \"yarn\", \"bun\"]);\nconst JS_TEST_RUNNERS = new Set([\"vitest\", \"jest\"]);\nconst SHARD_CONFLICT_FLAGS = [\"--shard\", \"--runInBand\", \"--coverage\", \"--watch\", \"-u\", \"--updateSnapshot\", \"-w\"];\n\nfunction classification(\n\tworkloadClass: WorkloadClass,\n\tcommandFamily: WorkloadCommandFamily,\n\toptions?: { readonly safeToAutoShard?: boolean; readonly extraReasons?: readonly string[] },\n): WorkloadClassification {\n\treturn {\n\t\tworkloadClass,\n\t\tcommandFamily,\n\t\tcomplexity: \"simple-argv\",\n\t\tsafeToAutoShard: options?.safeToAutoShard ?? false,\n\t\treasonCodes: [`family.${commandFamily}`, `class.${workloadClass}`, ...(options?.extraReasons ?? [])],\n\t};\n}\n\nfunction hasShardConflictFlag(argv: readonly string[]): boolean {\n\treturn argv.some((token) => SHARD_CONFLICT_FLAGS.some((flag) => token === flag || token.startsWith(`${flag}=`)));\n}\n\nexport function matchWorkloadFamily(argv: readonly string[]): WorkloadClassification {\n\tconst executable = basename(argv[0]);\n\tconst rest = argv.slice(1);\n\treturn (\n\t\tmatchToolchainFamily(executable, rest) ??\n\t\tmatchInfraFamily(executable, rest) ??\n\t\tmatchTierFallback(executable, rest)\n\t);\n}\n\n/** JS/TS/Go/Rust toolchains (§9.2 first block). */\nfunction matchToolchainFamily(executable: string, rest: readonly string[]): WorkloadClassification | null {\n\tif (JS_TEST_RUNNERS.has(executable)) {\n\t\t// §12.2: direct vitest/jest invocations are shard candidates unless a\n\t\t// conflicting flag changes semantics; the M5 planner enforces the rest.\n\t\tconst conflict = hasShardConflictFlag(rest);\n\t\treturn classification(\"heavy\", \"node-test\", {\n\t\t\tsafeToAutoShard: !conflict,\n\t\t\textraReasons: conflict ? [\"shard.flag.conflict\"] : [],\n\t\t});\n\t}\n\tif (PACKAGE_MANAGERS.has(executable)) {\n\t\tconst script = rest[0] === \"run\" ? rest[1] : rest[0];\n\t\tif (script === \"test\" || script === \"t\" || script?.startsWith(\"test\")) {\n\t\t\t// Opaque wrapper: the underlying runner is unknown statically (§12.1).\n\t\t\treturn classification(\"heavy\", \"node-test\", { extraReasons: [\"shard.wrapper.opaque\"] });\n\t\t}\n\t\tif (script === \"build\" || script?.startsWith(\"build\")) {\n\t\t\treturn classification(\"heavy\", \"node-build\");\n\t\t}\n\t\treturn classification(\"unknown\", \"generic-process\");\n\t}\n\tif (executable === \"tsc\") {\n\t\tconst hasProjectFlag = rest.some((token) => [\"-p\", \"--project\", \"-b\", \"--build\"].includes(token));\n\t\tconst hasFileArg = rest.some((token) => !token.startsWith(\"-\"));\n\t\tif (hasProjectFlag || !hasFileArg) {\n\t\t\treturn classification(\"heavy\", \"typescript\");\n\t\t}\n\t\treturn classification(\"cpu\", \"typescript\");\n\t}\n\tif (executable === \"go\" && rest[0] === \"test\") {\n\t\tconst packageArgs = rest.slice(1).filter((token) => !token.startsWith(\"-\"));\n\t\tconst recursive = packageArgs.some((token) => token.endsWith(\"...\"));\n\t\tif (recursive || packageArgs.length >= 2) {\n\t\t\treturn classification(\"heavy\", \"go-test\", { safeToAutoShard: recursive && !rest.includes(\"-run\") });\n\t\t}\n\t\treturn classification(\"cpu\", \"go-test\");\n\t}\n\tif (executable === \"cargo\" && [\"build\", \"test\", \"check\", \"clippy\"].includes(rest[0] ?? \"\")) {\n\t\treturn classification(\"heavy\", \"rust-build\");\n\t}\n\treturn null;\n}\n\n/** Container/monorepo/archive tooling (§9.2 second block). */\nfunction matchInfraFamily(executable: string, rest: readonly string[]): WorkloadClassification | null {\n\tif ((executable === \"docker\" || executable === \"podman\") && rest[0] === \"build\") {\n\t\treturn classification(\"heavy\", \"container-build\");\n\t}\n\tif ((executable === \"nx\" && rest[0] === \"run-many\") || (executable === \"turbo\" && rest[0] === \"run\")) {\n\t\treturn classification(\"heavy\", \"monorepo\");\n\t}\n\tif (executable === \"tar\") {\n\t\tconst flags = rest[0] ?? \"\";\n\t\tif (flags.includes(\"c\") || rest.includes(\"--create\")) {\n\t\t\treturn classification(\"heavy\", \"archive\");\n\t\t}\n\t\treturn classification(\"io\", \"archive\");\n\t}\n\tif ((executable === \"zip\" && rest.includes(\"-r\")) || (executable === \"7z\" && rest[0] === \"a\")) {\n\t\treturn classification(\"heavy\", \"archive\");\n\t}\n\treturn null;\n}\n\n/** Known light/io/cpu tiers, else unknown/generic-process. */\nfunction matchTierFallback(executable: string, rest: readonly string[]): WorkloadClassification {\n\tif (executable === \"git\" && LIGHT_GIT_SUBCOMMANDS.has(rest[0] ?? \"\")) {\n\t\treturn classification(\"light\", \"generic-process\");\n\t}\n\tif (LIGHT_EXECUTABLES.has(executable)) {\n\t\treturn classification(\"light\", \"generic-process\");\n\t}\n\tif (IO_EXECUTABLES.has(executable)) {\n\t\treturn classification(\"io\", \"generic-process\");\n\t}\n\tif (CPU_EXECUTABLES.has(executable)) {\n\t\treturn classification(\"cpu\", \"generic-process\");\n\t}\n\treturn classification(\"unknown\", \"generic-process\");\n}\n\nfunction basename(token: string): string {\n\tconst slash = token.lastIndexOf(\"/\");\n\treturn slash === -1 ? token : token.slice(slash + 1);\n}\n"]}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const LIGHT_EXECUTABLES = new Set([
|
|
2
|
+
"ls",
|
|
3
|
+
"cat",
|
|
4
|
+
"head",
|
|
5
|
+
"tail",
|
|
6
|
+
"grep",
|
|
7
|
+
"rg",
|
|
8
|
+
"find",
|
|
9
|
+
"echo",
|
|
10
|
+
"pwd",
|
|
11
|
+
"which",
|
|
12
|
+
"wc",
|
|
13
|
+
"stat",
|
|
14
|
+
"file",
|
|
15
|
+
"true",
|
|
16
|
+
"false",
|
|
17
|
+
]);
|
|
18
|
+
const LIGHT_GIT_SUBCOMMANDS = new Set(["status", "log", "diff", "show", "branch", "rev-parse", "blame"]);
|
|
19
|
+
const IO_EXECUTABLES = new Set(["cp", "mv", "rsync", "mkdir", "touch", "dd"]);
|
|
20
|
+
const CPU_EXECUTABLES = new Set(["eslint", "prettier", "biome", "ruff", "gofmt", "rustfmt"]);
|
|
21
|
+
const PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn", "bun"]);
|
|
22
|
+
const JS_TEST_RUNNERS = new Set(["vitest", "jest"]);
|
|
23
|
+
const SHARD_CONFLICT_FLAGS = ["--shard", "--runInBand", "--coverage", "--watch", "-u", "--updateSnapshot", "-w"];
|
|
24
|
+
function classification(workloadClass, commandFamily, options) {
|
|
25
|
+
return {
|
|
26
|
+
workloadClass,
|
|
27
|
+
commandFamily,
|
|
28
|
+
complexity: "simple-argv",
|
|
29
|
+
safeToAutoShard: options?.safeToAutoShard ?? false,
|
|
30
|
+
reasonCodes: [`family.${commandFamily}`, `class.${workloadClass}`, ...(options?.extraReasons ?? [])],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function hasShardConflictFlag(argv) {
|
|
34
|
+
return argv.some((token) => SHARD_CONFLICT_FLAGS.some((flag) => token === flag || token.startsWith(`${flag}=`)));
|
|
35
|
+
}
|
|
36
|
+
export function matchWorkloadFamily(argv) {
|
|
37
|
+
const executable = basename(argv[0]);
|
|
38
|
+
const rest = argv.slice(1);
|
|
39
|
+
return (matchToolchainFamily(executable, rest) ??
|
|
40
|
+
matchInfraFamily(executable, rest) ??
|
|
41
|
+
matchTierFallback(executable, rest));
|
|
42
|
+
}
|
|
43
|
+
/** JS/TS/Go/Rust toolchains (§9.2 first block). */
|
|
44
|
+
function matchToolchainFamily(executable, rest) {
|
|
45
|
+
if (JS_TEST_RUNNERS.has(executable)) {
|
|
46
|
+
// §12.2: direct vitest/jest invocations are shard candidates unless a
|
|
47
|
+
// conflicting flag changes semantics; the M5 planner enforces the rest.
|
|
48
|
+
const conflict = hasShardConflictFlag(rest);
|
|
49
|
+
return classification("heavy", "node-test", {
|
|
50
|
+
safeToAutoShard: !conflict,
|
|
51
|
+
extraReasons: conflict ? ["shard.flag.conflict"] : [],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (PACKAGE_MANAGERS.has(executable)) {
|
|
55
|
+
const script = rest[0] === "run" ? rest[1] : rest[0];
|
|
56
|
+
if (script === "test" || script === "t" || script?.startsWith("test")) {
|
|
57
|
+
// Opaque wrapper: the underlying runner is unknown statically (§12.1).
|
|
58
|
+
return classification("heavy", "node-test", { extraReasons: ["shard.wrapper.opaque"] });
|
|
59
|
+
}
|
|
60
|
+
if (script === "build" || script?.startsWith("build")) {
|
|
61
|
+
return classification("heavy", "node-build");
|
|
62
|
+
}
|
|
63
|
+
return classification("unknown", "generic-process");
|
|
64
|
+
}
|
|
65
|
+
if (executable === "tsc") {
|
|
66
|
+
const hasProjectFlag = rest.some((token) => ["-p", "--project", "-b", "--build"].includes(token));
|
|
67
|
+
const hasFileArg = rest.some((token) => !token.startsWith("-"));
|
|
68
|
+
if (hasProjectFlag || !hasFileArg) {
|
|
69
|
+
return classification("heavy", "typescript");
|
|
70
|
+
}
|
|
71
|
+
return classification("cpu", "typescript");
|
|
72
|
+
}
|
|
73
|
+
if (executable === "go" && rest[0] === "test") {
|
|
74
|
+
const packageArgs = rest.slice(1).filter((token) => !token.startsWith("-"));
|
|
75
|
+
const recursive = packageArgs.some((token) => token.endsWith("..."));
|
|
76
|
+
if (recursive || packageArgs.length >= 2) {
|
|
77
|
+
return classification("heavy", "go-test", { safeToAutoShard: recursive && !rest.includes("-run") });
|
|
78
|
+
}
|
|
79
|
+
return classification("cpu", "go-test");
|
|
80
|
+
}
|
|
81
|
+
if (executable === "cargo" && ["build", "test", "check", "clippy"].includes(rest[0] ?? "")) {
|
|
82
|
+
return classification("heavy", "rust-build");
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/** Container/monorepo/archive tooling (§9.2 second block). */
|
|
87
|
+
function matchInfraFamily(executable, rest) {
|
|
88
|
+
if ((executable === "docker" || executable === "podman") && rest[0] === "build") {
|
|
89
|
+
return classification("heavy", "container-build");
|
|
90
|
+
}
|
|
91
|
+
if ((executable === "nx" && rest[0] === "run-many") || (executable === "turbo" && rest[0] === "run")) {
|
|
92
|
+
return classification("heavy", "monorepo");
|
|
93
|
+
}
|
|
94
|
+
if (executable === "tar") {
|
|
95
|
+
const flags = rest[0] ?? "";
|
|
96
|
+
if (flags.includes("c") || rest.includes("--create")) {
|
|
97
|
+
return classification("heavy", "archive");
|
|
98
|
+
}
|
|
99
|
+
return classification("io", "archive");
|
|
100
|
+
}
|
|
101
|
+
if ((executable === "zip" && rest.includes("-r")) || (executable === "7z" && rest[0] === "a")) {
|
|
102
|
+
return classification("heavy", "archive");
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
/** Known light/io/cpu tiers, else unknown/generic-process. */
|
|
107
|
+
function matchTierFallback(executable, rest) {
|
|
108
|
+
if (executable === "git" && LIGHT_GIT_SUBCOMMANDS.has(rest[0] ?? "")) {
|
|
109
|
+
return classification("light", "generic-process");
|
|
110
|
+
}
|
|
111
|
+
if (LIGHT_EXECUTABLES.has(executable)) {
|
|
112
|
+
return classification("light", "generic-process");
|
|
113
|
+
}
|
|
114
|
+
if (IO_EXECUTABLES.has(executable)) {
|
|
115
|
+
return classification("io", "generic-process");
|
|
116
|
+
}
|
|
117
|
+
if (CPU_EXECUTABLES.has(executable)) {
|
|
118
|
+
return classification("cpu", "generic-process");
|
|
119
|
+
}
|
|
120
|
+
return classification("unknown", "generic-process");
|
|
121
|
+
}
|
|
122
|
+
function basename(token) {
|
|
123
|
+
const slash = token.lastIndexOf("/");
|
|
124
|
+
return slash === -1 ? token : token.slice(slash + 1);
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=workload-family-matcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workload-family-matcher.js","sourceRoot":"","sources":["../../src/core/workload-family-matcher.ts"],"names":[],"mappings":"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","sourcesContent":["import type { WorkloadClass, WorkloadClassification, WorkloadCommandFamily } from \"./workload-classifier.ts\";\n\nconst LIGHT_EXECUTABLES = new Set([\n\t\"ls\",\n\t\"cat\",\n\t\"head\",\n\t\"tail\",\n\t\"grep\",\n\t\"rg\",\n\t\"find\",\n\t\"echo\",\n\t\"pwd\",\n\t\"which\",\n\t\"wc\",\n\t\"stat\",\n\t\"file\",\n\t\"true\",\n\t\"false\",\n]);\nconst LIGHT_GIT_SUBCOMMANDS = new Set([\"status\", \"log\", \"diff\", \"show\", \"branch\", \"rev-parse\", \"blame\"]);\nconst IO_EXECUTABLES = new Set([\"cp\", \"mv\", \"rsync\", \"mkdir\", \"touch\", \"dd\"]);\nconst CPU_EXECUTABLES = new Set([\"eslint\", \"prettier\", \"biome\", \"ruff\", \"gofmt\", \"rustfmt\"]);\nconst PACKAGE_MANAGERS = new Set([\"npm\", \"pnpm\", \"yarn\", \"bun\"]);\nconst JS_TEST_RUNNERS = new Set([\"vitest\", \"jest\"]);\nconst SHARD_CONFLICT_FLAGS = [\"--shard\", \"--runInBand\", \"--coverage\", \"--watch\", \"-u\", \"--updateSnapshot\", \"-w\"];\n\nfunction classification(\n\tworkloadClass: WorkloadClass,\n\tcommandFamily: WorkloadCommandFamily,\n\toptions?: { readonly safeToAutoShard?: boolean; readonly extraReasons?: readonly string[] },\n): WorkloadClassification {\n\treturn {\n\t\tworkloadClass,\n\t\tcommandFamily,\n\t\tcomplexity: \"simple-argv\",\n\t\tsafeToAutoShard: options?.safeToAutoShard ?? false,\n\t\treasonCodes: [`family.${commandFamily}`, `class.${workloadClass}`, ...(options?.extraReasons ?? [])],\n\t};\n}\n\nfunction hasShardConflictFlag(argv: readonly string[]): boolean {\n\treturn argv.some((token) => SHARD_CONFLICT_FLAGS.some((flag) => token === flag || token.startsWith(`${flag}=`)));\n}\n\nexport function matchWorkloadFamily(argv: readonly string[]): WorkloadClassification {\n\tconst executable = basename(argv[0]);\n\tconst rest = argv.slice(1);\n\treturn (\n\t\tmatchToolchainFamily(executable, rest) ??\n\t\tmatchInfraFamily(executable, rest) ??\n\t\tmatchTierFallback(executable, rest)\n\t);\n}\n\n/** JS/TS/Go/Rust toolchains (§9.2 first block). */\nfunction matchToolchainFamily(executable: string, rest: readonly string[]): WorkloadClassification | null {\n\tif (JS_TEST_RUNNERS.has(executable)) {\n\t\t// §12.2: direct vitest/jest invocations are shard candidates unless a\n\t\t// conflicting flag changes semantics; the M5 planner enforces the rest.\n\t\tconst conflict = hasShardConflictFlag(rest);\n\t\treturn classification(\"heavy\", \"node-test\", {\n\t\t\tsafeToAutoShard: !conflict,\n\t\t\textraReasons: conflict ? [\"shard.flag.conflict\"] : [],\n\t\t});\n\t}\n\tif (PACKAGE_MANAGERS.has(executable)) {\n\t\tconst script = rest[0] === \"run\" ? rest[1] : rest[0];\n\t\tif (script === \"test\" || script === \"t\" || script?.startsWith(\"test\")) {\n\t\t\t// Opaque wrapper: the underlying runner is unknown statically (§12.1).\n\t\t\treturn classification(\"heavy\", \"node-test\", { extraReasons: [\"shard.wrapper.opaque\"] });\n\t\t}\n\t\tif (script === \"build\" || script?.startsWith(\"build\")) {\n\t\t\treturn classification(\"heavy\", \"node-build\");\n\t\t}\n\t\treturn classification(\"unknown\", \"generic-process\");\n\t}\n\tif (executable === \"tsc\") {\n\t\tconst hasProjectFlag = rest.some((token) => [\"-p\", \"--project\", \"-b\", \"--build\"].includes(token));\n\t\tconst hasFileArg = rest.some((token) => !token.startsWith(\"-\"));\n\t\tif (hasProjectFlag || !hasFileArg) {\n\t\t\treturn classification(\"heavy\", \"typescript\");\n\t\t}\n\t\treturn classification(\"cpu\", \"typescript\");\n\t}\n\tif (executable === \"go\" && rest[0] === \"test\") {\n\t\tconst packageArgs = rest.slice(1).filter((token) => !token.startsWith(\"-\"));\n\t\tconst recursive = packageArgs.some((token) => token.endsWith(\"...\"));\n\t\tif (recursive || packageArgs.length >= 2) {\n\t\t\treturn classification(\"heavy\", \"go-test\", { safeToAutoShard: recursive && !rest.includes(\"-run\") });\n\t\t}\n\t\treturn classification(\"cpu\", \"go-test\");\n\t}\n\tif (executable === \"cargo\" && [\"build\", \"test\", \"check\", \"clippy\"].includes(rest[0] ?? \"\")) {\n\t\treturn classification(\"heavy\", \"rust-build\");\n\t}\n\treturn null;\n}\n\n/** Container/monorepo/archive tooling (§9.2 second block). */\nfunction matchInfraFamily(executable: string, rest: readonly string[]): WorkloadClassification | null {\n\tif ((executable === \"docker\" || executable === \"podman\") && rest[0] === \"build\") {\n\t\treturn classification(\"heavy\", \"container-build\");\n\t}\n\tif ((executable === \"nx\" && rest[0] === \"run-many\") || (executable === \"turbo\" && rest[0] === \"run\")) {\n\t\treturn classification(\"heavy\", \"monorepo\");\n\t}\n\tif (executable === \"tar\") {\n\t\tconst flags = rest[0] ?? \"\";\n\t\tif (flags.includes(\"c\") || rest.includes(\"--create\")) {\n\t\t\treturn classification(\"heavy\", \"archive\");\n\t\t}\n\t\treturn classification(\"io\", \"archive\");\n\t}\n\tif ((executable === \"zip\" && rest.includes(\"-r\")) || (executable === \"7z\" && rest[0] === \"a\")) {\n\t\treturn classification(\"heavy\", \"archive\");\n\t}\n\treturn null;\n}\n\n/** Known light/io/cpu tiers, else unknown/generic-process. */\nfunction matchTierFallback(executable: string, rest: readonly string[]): WorkloadClassification {\n\tif (executable === \"git\" && LIGHT_GIT_SUBCOMMANDS.has(rest[0] ?? \"\")) {\n\t\treturn classification(\"light\", \"generic-process\");\n\t}\n\tif (LIGHT_EXECUTABLES.has(executable)) {\n\t\treturn classification(\"light\", \"generic-process\");\n\t}\n\tif (IO_EXECUTABLES.has(executable)) {\n\t\treturn classification(\"io\", \"generic-process\");\n\t}\n\tif (CPU_EXECUTABLES.has(executable)) {\n\t\treturn classification(\"cpu\", \"generic-process\");\n\t}\n\treturn classification(\"unknown\", \"generic-process\");\n}\n\nfunction basename(token: string): string {\n\tconst slash = token.lastIndexOf(\"/\");\n\treturn slash === -1 ? token : token.slice(slash + 1);\n}\n"]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { WorkloadClass } from "./workload-classifier.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Shared workload permit pool (OMK v0.97.x roadmap §10, M3/PR5).
|
|
4
|
+
*
|
|
5
|
+
* Bounds concurrent heavy/cpu subprocesses independently of tool concurrency
|
|
6
|
+
* (§10.1: 4 concurrent reads != 4 concurrent `npm test` processes). One pool
|
|
7
|
+
* instance is shared by a session and, later, its child subagents (§14.1) —
|
|
8
|
+
* a child creating its own pool would oversubscribe the host.
|
|
9
|
+
*
|
|
10
|
+
* §10.3 rules implemented here: strict FIFO (§10.4), abort- and
|
|
11
|
+
* timeout-aware waiting, exactly-once release with double-release recorded
|
|
12
|
+
* as a diagnostic no-op, immediate rejection of requests wider than
|
|
13
|
+
* capacity, and a hard queue cap whose overflow surfaces as a structured
|
|
14
|
+
* error code (§15.4 `queue_overflow`). Waiter timers stay ref'd — a permit
|
|
15
|
+
* wait is actively awaited work — and are cleared on settle.
|
|
16
|
+
*/
|
|
17
|
+
export interface WorkloadPermitRequest {
|
|
18
|
+
readonly requestId: string;
|
|
19
|
+
readonly promptRunId: string;
|
|
20
|
+
readonly workloadClass: WorkloadClass;
|
|
21
|
+
readonly weight: 1 | 2;
|
|
22
|
+
readonly signal?: AbortSignal;
|
|
23
|
+
readonly timeoutMs?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface WorkloadPermit {
|
|
26
|
+
readonly permitId: string;
|
|
27
|
+
readonly requestId: string;
|
|
28
|
+
readonly acquiredAt: string;
|
|
29
|
+
release(): void;
|
|
30
|
+
}
|
|
31
|
+
export interface WorkloadPermitPoolSnapshot {
|
|
32
|
+
readonly capacity: number;
|
|
33
|
+
readonly activeWeight: number;
|
|
34
|
+
readonly queuedCount: number;
|
|
35
|
+
}
|
|
36
|
+
export type WorkloadPermitErrorCode = "queue_overflow" | "timeout" | "aborted" | "over_capacity_weight";
|
|
37
|
+
export declare class WorkloadPermitError extends Error {
|
|
38
|
+
readonly code: WorkloadPermitErrorCode;
|
|
39
|
+
readonly requestId: string;
|
|
40
|
+
constructor(code: WorkloadPermitErrorCode, requestId: string);
|
|
41
|
+
}
|
|
42
|
+
export declare const DEFAULT_PERMIT_QUEUE_CAP = 32;
|
|
43
|
+
export type WorkloadPermitPoolOptions = {
|
|
44
|
+
/** Concurrent weight budget; §7.2 heavy caps feed this. Default 2. */
|
|
45
|
+
capacity?: number;
|
|
46
|
+
/** §10.3 queue length hard cap. Default 32. */
|
|
47
|
+
maxQueue?: number;
|
|
48
|
+
/** Injectable clock for deterministic tests. */
|
|
49
|
+
now?: () => Date;
|
|
50
|
+
};
|
|
51
|
+
export declare class WorkloadPermitPool {
|
|
52
|
+
private capacity;
|
|
53
|
+
private readonly maxQueue;
|
|
54
|
+
private readonly now;
|
|
55
|
+
private activeWeight;
|
|
56
|
+
private readonly queue;
|
|
57
|
+
private readonly releasedPermitIds;
|
|
58
|
+
private readonly activePermitIds;
|
|
59
|
+
private doubleReleases;
|
|
60
|
+
constructor(options?: WorkloadPermitPoolOptions);
|
|
61
|
+
/**
|
|
62
|
+
* Update capacity from a new admission decision. In-flight permits are
|
|
63
|
+
* never revoked; a reduction only delays future grants until active
|
|
64
|
+
* weight drains below the new capacity.
|
|
65
|
+
*/
|
|
66
|
+
setCapacity(capacity: number): void;
|
|
67
|
+
/** Acquire a permit. Strict FIFO; rejects with {@link WorkloadPermitError}. */
|
|
68
|
+
acquire(request: WorkloadPermitRequest): Promise<WorkloadPermit>;
|
|
69
|
+
snapshot(): WorkloadPermitPoolSnapshot;
|
|
70
|
+
/** Diagnostic count of double releases (§10.3 no-op + diagnostic). */
|
|
71
|
+
get doubleReleaseCount(): number;
|
|
72
|
+
private grant;
|
|
73
|
+
/** Grant queued waiters strictly in FIFO order while the head fits (§10.4). */
|
|
74
|
+
private grantWhilePossible;
|
|
75
|
+
private evict;
|
|
76
|
+
private settleCleanup;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=workload-permit-pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workload-permit-pool.d.ts","sourceRoot":"","sources":["../../src/core/workload-permit-pool.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,OAAO,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,0BAA0B;IAC1C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,MAAM,uBAAuB,GAAG,gBAAgB,GAAG,SAAS,GAAG,SAAS,GAAG,sBAAsB,CAAC;AAExG,qBAAa,mBAAoB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B,YAAY,IAAI,EAAE,uBAAuB,EAAE,SAAS,EAAE,MAAM,EAK3D;CACD;AAUD,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAE3C,MAAM,MAAM,yBAAyB,GAAG;IACvC,uEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gDAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CACjB,CAAC;AAEF,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAa;IACjC,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAqB;IACvD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IACrD,OAAO,CAAC,cAAc,CAAK;IAE3B,YAAY,OAAO,CAAC,EAAE,yBAAyB,EAI9C;IAED;;;;OAIG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAGlC;IAED,+EAA+E;IAC/E,OAAO,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC,CA6B/D;IAED,QAAQ,IAAI,0BAA0B,CAErC;IAED,uEAAsE;IACtE,IAAI,kBAAkB,IAAI,MAAM,CAE/B;IAED,OAAO,CAAC,KAAK;IAmBb,gFAA+E;IAC/E,OAAO,CAAC,kBAAkB;IAoB1B,OAAO,CAAC,KAAK;IAUb,OAAO,CAAC,aAAa;CAUrB","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { WorkloadClass } from \"./workload-classifier.ts\";\n\n/**\n * Shared workload permit pool (OMK v0.97.x roadmap §10, M3/PR5).\n *\n * Bounds concurrent heavy/cpu subprocesses independently of tool concurrency\n * (§10.1: 4 concurrent reads != 4 concurrent `npm test` processes). One pool\n * instance is shared by a session and, later, its child subagents (§14.1) —\n * a child creating its own pool would oversubscribe the host.\n *\n * §10.3 rules implemented here: strict FIFO (§10.4), abort- and\n * timeout-aware waiting, exactly-once release with double-release recorded\n * as a diagnostic no-op, immediate rejection of requests wider than\n * capacity, and a hard queue cap whose overflow surfaces as a structured\n * error code (§15.4 `queue_overflow`). Waiter timers stay ref'd — a permit\n * wait is actively awaited work — and are cleared on settle.\n */\n\nexport interface WorkloadPermitRequest {\n\treadonly requestId: string;\n\treadonly promptRunId: string;\n\treadonly workloadClass: WorkloadClass;\n\treadonly weight: 1 | 2;\n\treadonly signal?: AbortSignal;\n\treadonly timeoutMs?: number;\n}\n\nexport interface WorkloadPermit {\n\treadonly permitId: string;\n\treadonly requestId: string;\n\treadonly acquiredAt: string;\n\trelease(): void;\n}\n\nexport interface WorkloadPermitPoolSnapshot {\n\treadonly capacity: number;\n\treadonly activeWeight: number;\n\treadonly queuedCount: number;\n}\n\nexport type WorkloadPermitErrorCode = \"queue_overflow\" | \"timeout\" | \"aborted\" | \"over_capacity_weight\";\n\nexport class WorkloadPermitError extends Error {\n\treadonly code: WorkloadPermitErrorCode;\n\treadonly requestId: string;\n\n\tconstructor(code: WorkloadPermitErrorCode, requestId: string) {\n\t\tsuper(`workload permit ${code} (request ${requestId})`);\n\t\tthis.name = \"WorkloadPermitError\";\n\t\tthis.code = code;\n\t\tthis.requestId = requestId;\n\t}\n}\n\ninterface Waiter {\n\treadonly request: WorkloadPermitRequest;\n\treadonly resolve: (permit: WorkloadPermit) => void;\n\treadonly reject: (error: WorkloadPermitError) => void;\n\ttimer: ReturnType<typeof setTimeout> | null;\n\tabortListener: (() => void) | null;\n}\n\nexport const DEFAULT_PERMIT_QUEUE_CAP = 32;\n\nexport type WorkloadPermitPoolOptions = {\n\t/** Concurrent weight budget; §7.2 heavy caps feed this. Default 2. */\n\tcapacity?: number;\n\t/** §10.3 queue length hard cap. Default 32. */\n\tmaxQueue?: number;\n\t/** Injectable clock for deterministic tests. */\n\tnow?: () => Date;\n};\n\nexport class WorkloadPermitPool {\n\tprivate capacity: number;\n\tprivate readonly maxQueue: number;\n\tprivate readonly now: () => Date;\n\tprivate activeWeight = 0;\n\tprivate readonly queue: Waiter[] = [];\n\tprivate readonly releasedPermitIds = new Set<string>();\n\tprivate readonly activePermitIds = new Set<string>();\n\tprivate doubleReleases = 0;\n\n\tconstructor(options?: WorkloadPermitPoolOptions) {\n\t\tthis.capacity = sanitizeCapacity(options?.capacity, 2);\n\t\tthis.maxQueue = sanitizeCapacity(options?.maxQueue, DEFAULT_PERMIT_QUEUE_CAP);\n\t\tthis.now = options?.now ?? (() => new Date());\n\t}\n\n\t/**\n\t * Update capacity from a new admission decision. In-flight permits are\n\t * never revoked; a reduction only delays future grants until active\n\t * weight drains below the new capacity.\n\t */\n\tsetCapacity(capacity: number): void {\n\t\tthis.capacity = sanitizeCapacity(capacity, this.capacity);\n\t\tthis.grantWhilePossible();\n\t}\n\n\t/** Acquire a permit. Strict FIFO; rejects with {@link WorkloadPermitError}. */\n\tacquire(request: WorkloadPermitRequest): Promise<WorkloadPermit> {\n\t\tif (request.weight > this.capacity) {\n\t\t\treturn Promise.reject(new WorkloadPermitError(\"over_capacity_weight\", request.requestId));\n\t\t}\n\t\tif (request.signal?.aborted) {\n\t\t\treturn Promise.reject(new WorkloadPermitError(\"aborted\", request.requestId));\n\t\t}\n\t\tif (this.queue.length === 0 && this.activeWeight + request.weight <= this.capacity) {\n\t\t\treturn Promise.resolve(this.grant(request));\n\t\t}\n\t\tif (this.queue.length >= this.maxQueue) {\n\t\t\treturn Promise.reject(new WorkloadPermitError(\"queue_overflow\", request.requestId));\n\t\t}\n\t\treturn new Promise<WorkloadPermit>((resolve, reject) => {\n\t\t\tconst waiter: Waiter = { request, resolve, reject, timer: null, abortListener: null };\n\t\t\tif (typeof request.timeoutMs === \"number\" && Number.isFinite(request.timeoutMs) && request.timeoutMs > 0) {\n\t\t\t\twaiter.timer = setTimeout(() => {\n\t\t\t\t\tthis.evict(waiter, \"timeout\");\n\t\t\t\t}, request.timeoutMs);\n\t\t\t}\n\t\t\tif (request.signal) {\n\t\t\t\tconst signal = request.signal;\n\t\t\t\twaiter.abortListener = () => {\n\t\t\t\t\tthis.evict(waiter, \"aborted\");\n\t\t\t\t};\n\t\t\t\tsignal.addEventListener(\"abort\", waiter.abortListener, { once: true });\n\t\t\t}\n\t\t\tthis.queue.push(waiter);\n\t\t});\n\t}\n\n\tsnapshot(): WorkloadPermitPoolSnapshot {\n\t\treturn { capacity: this.capacity, activeWeight: this.activeWeight, queuedCount: this.queue.length };\n\t}\n\n\t/** Diagnostic count of double releases (§10.3 no-op + diagnostic). */\n\tget doubleReleaseCount(): number {\n\t\treturn this.doubleReleases;\n\t}\n\n\tprivate grant(request: WorkloadPermitRequest): WorkloadPermit {\n\t\tthis.activeWeight += request.weight;\n\t\tconst permitId = `permit-${randomUUID()}`;\n\t\tthis.activePermitIds.add(permitId);\n\t\tconst release = (): void => {\n\t\t\t// §10.3 exactly-once: a late or repeated settlement must not return\n\t\t\t// weight it no longer owns.\n\t\t\tif (this.releasedPermitIds.has(permitId) || !this.activePermitIds.has(permitId)) {\n\t\t\t\tthis.doubleReleases += 1;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.activePermitIds.delete(permitId);\n\t\t\tthis.releasedPermitIds.add(permitId);\n\t\t\tthis.activeWeight = Math.max(0, this.activeWeight - request.weight);\n\t\t\tthis.grantWhilePossible();\n\t\t};\n\t\treturn { permitId, requestId: request.requestId, acquiredAt: this.now().toISOString(), release };\n\t}\n\n\t/** Grant queued waiters strictly in FIFO order while the head fits (§10.4). */\n\tprivate grantWhilePossible(): void {\n\t\twhile (this.queue.length > 0) {\n\t\t\tconst head = this.queue[0];\n\t\t\tif (head.request.weight > this.capacity) {\n\t\t\t\t// Capacity shrank below the head's weight: reject rather than\n\t\t\t\t// starving everyone behind it forever.\n\t\t\t\tthis.queue.shift();\n\t\t\t\tthis.settleCleanup(head);\n\t\t\t\thead.reject(new WorkloadPermitError(\"over_capacity_weight\", head.request.requestId));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (this.activeWeight + head.request.weight > this.capacity) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.queue.shift();\n\t\t\tthis.settleCleanup(head);\n\t\t\thead.resolve(this.grant(head.request));\n\t\t}\n\t}\n\n\tprivate evict(waiter: Waiter, code: WorkloadPermitErrorCode): void {\n\t\tconst index = this.queue.indexOf(waiter);\n\t\tif (index === -1) {\n\t\t\treturn; // Already granted or already evicted.\n\t\t}\n\t\tthis.queue.splice(index, 1);\n\t\tthis.settleCleanup(waiter);\n\t\twaiter.reject(new WorkloadPermitError(code, waiter.request.requestId));\n\t}\n\n\tprivate settleCleanup(waiter: Waiter): void {\n\t\tif (waiter.timer !== null) {\n\t\t\tclearTimeout(waiter.timer);\n\t\t\twaiter.timer = null;\n\t\t}\n\t\tif (waiter.abortListener !== null) {\n\t\t\twaiter.request.signal?.removeEventListener(\"abort\", waiter.abortListener);\n\t\t\twaiter.abortListener = null;\n\t\t}\n\t}\n}\n\nfunction sanitizeCapacity(value: number | undefined, fallback: number): number {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 1) {\n\t\treturn fallback;\n\t}\n\treturn Math.floor(value);\n}\n"]}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export class WorkloadPermitError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
requestId;
|
|
5
|
+
constructor(code, requestId) {
|
|
6
|
+
super(`workload permit ${code} (request ${requestId})`);
|
|
7
|
+
this.name = "WorkloadPermitError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.requestId = requestId;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export const DEFAULT_PERMIT_QUEUE_CAP = 32;
|
|
13
|
+
export class WorkloadPermitPool {
|
|
14
|
+
capacity;
|
|
15
|
+
maxQueue;
|
|
16
|
+
now;
|
|
17
|
+
activeWeight = 0;
|
|
18
|
+
queue = [];
|
|
19
|
+
releasedPermitIds = new Set();
|
|
20
|
+
activePermitIds = new Set();
|
|
21
|
+
doubleReleases = 0;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.capacity = sanitizeCapacity(options?.capacity, 2);
|
|
24
|
+
this.maxQueue = sanitizeCapacity(options?.maxQueue, DEFAULT_PERMIT_QUEUE_CAP);
|
|
25
|
+
this.now = options?.now ?? (() => new Date());
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Update capacity from a new admission decision. In-flight permits are
|
|
29
|
+
* never revoked; a reduction only delays future grants until active
|
|
30
|
+
* weight drains below the new capacity.
|
|
31
|
+
*/
|
|
32
|
+
setCapacity(capacity) {
|
|
33
|
+
this.capacity = sanitizeCapacity(capacity, this.capacity);
|
|
34
|
+
this.grantWhilePossible();
|
|
35
|
+
}
|
|
36
|
+
/** Acquire a permit. Strict FIFO; rejects with {@link WorkloadPermitError}. */
|
|
37
|
+
acquire(request) {
|
|
38
|
+
if (request.weight > this.capacity) {
|
|
39
|
+
return Promise.reject(new WorkloadPermitError("over_capacity_weight", request.requestId));
|
|
40
|
+
}
|
|
41
|
+
if (request.signal?.aborted) {
|
|
42
|
+
return Promise.reject(new WorkloadPermitError("aborted", request.requestId));
|
|
43
|
+
}
|
|
44
|
+
if (this.queue.length === 0 && this.activeWeight + request.weight <= this.capacity) {
|
|
45
|
+
return Promise.resolve(this.grant(request));
|
|
46
|
+
}
|
|
47
|
+
if (this.queue.length >= this.maxQueue) {
|
|
48
|
+
return Promise.reject(new WorkloadPermitError("queue_overflow", request.requestId));
|
|
49
|
+
}
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const waiter = { request, resolve, reject, timer: null, abortListener: null };
|
|
52
|
+
if (typeof request.timeoutMs === "number" && Number.isFinite(request.timeoutMs) && request.timeoutMs > 0) {
|
|
53
|
+
waiter.timer = setTimeout(() => {
|
|
54
|
+
this.evict(waiter, "timeout");
|
|
55
|
+
}, request.timeoutMs);
|
|
56
|
+
}
|
|
57
|
+
if (request.signal) {
|
|
58
|
+
const signal = request.signal;
|
|
59
|
+
waiter.abortListener = () => {
|
|
60
|
+
this.evict(waiter, "aborted");
|
|
61
|
+
};
|
|
62
|
+
signal.addEventListener("abort", waiter.abortListener, { once: true });
|
|
63
|
+
}
|
|
64
|
+
this.queue.push(waiter);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
snapshot() {
|
|
68
|
+
return { capacity: this.capacity, activeWeight: this.activeWeight, queuedCount: this.queue.length };
|
|
69
|
+
}
|
|
70
|
+
/** Diagnostic count of double releases (§10.3 no-op + diagnostic). */
|
|
71
|
+
get doubleReleaseCount() {
|
|
72
|
+
return this.doubleReleases;
|
|
73
|
+
}
|
|
74
|
+
grant(request) {
|
|
75
|
+
this.activeWeight += request.weight;
|
|
76
|
+
const permitId = `permit-${randomUUID()}`;
|
|
77
|
+
this.activePermitIds.add(permitId);
|
|
78
|
+
const release = () => {
|
|
79
|
+
// §10.3 exactly-once: a late or repeated settlement must not return
|
|
80
|
+
// weight it no longer owns.
|
|
81
|
+
if (this.releasedPermitIds.has(permitId) || !this.activePermitIds.has(permitId)) {
|
|
82
|
+
this.doubleReleases += 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.activePermitIds.delete(permitId);
|
|
86
|
+
this.releasedPermitIds.add(permitId);
|
|
87
|
+
this.activeWeight = Math.max(0, this.activeWeight - request.weight);
|
|
88
|
+
this.grantWhilePossible();
|
|
89
|
+
};
|
|
90
|
+
return { permitId, requestId: request.requestId, acquiredAt: this.now().toISOString(), release };
|
|
91
|
+
}
|
|
92
|
+
/** Grant queued waiters strictly in FIFO order while the head fits (§10.4). */
|
|
93
|
+
grantWhilePossible() {
|
|
94
|
+
while (this.queue.length > 0) {
|
|
95
|
+
const head = this.queue[0];
|
|
96
|
+
if (head.request.weight > this.capacity) {
|
|
97
|
+
// Capacity shrank below the head's weight: reject rather than
|
|
98
|
+
// starving everyone behind it forever.
|
|
99
|
+
this.queue.shift();
|
|
100
|
+
this.settleCleanup(head);
|
|
101
|
+
head.reject(new WorkloadPermitError("over_capacity_weight", head.request.requestId));
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (this.activeWeight + head.request.weight > this.capacity) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this.queue.shift();
|
|
108
|
+
this.settleCleanup(head);
|
|
109
|
+
head.resolve(this.grant(head.request));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
evict(waiter, code) {
|
|
113
|
+
const index = this.queue.indexOf(waiter);
|
|
114
|
+
if (index === -1) {
|
|
115
|
+
return; // Already granted or already evicted.
|
|
116
|
+
}
|
|
117
|
+
this.queue.splice(index, 1);
|
|
118
|
+
this.settleCleanup(waiter);
|
|
119
|
+
waiter.reject(new WorkloadPermitError(code, waiter.request.requestId));
|
|
120
|
+
}
|
|
121
|
+
settleCleanup(waiter) {
|
|
122
|
+
if (waiter.timer !== null) {
|
|
123
|
+
clearTimeout(waiter.timer);
|
|
124
|
+
waiter.timer = null;
|
|
125
|
+
}
|
|
126
|
+
if (waiter.abortListener !== null) {
|
|
127
|
+
waiter.request.signal?.removeEventListener("abort", waiter.abortListener);
|
|
128
|
+
waiter.abortListener = null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function sanitizeCapacity(value, fallback) {
|
|
133
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 1) {
|
|
134
|
+
return fallback;
|
|
135
|
+
}
|
|
136
|
+
return Math.floor(value);
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=workload-permit-pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workload-permit-pool.js","sourceRoot":"","sources":["../../src/core/workload-permit-pool.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { WorkloadClass } from \"./workload-classifier.ts\";\n\n/**\n * Shared workload permit pool (OMK v0.97.x roadmap §10, M3/PR5).\n *\n * Bounds concurrent heavy/cpu subprocesses independently of tool concurrency\n * (§10.1: 4 concurrent reads != 4 concurrent `npm test` processes). One pool\n * instance is shared by a session and, later, its child subagents (§14.1) —\n * a child creating its own pool would oversubscribe the host.\n *\n * §10.3 rules implemented here: strict FIFO (§10.4), abort- and\n * timeout-aware waiting, exactly-once release with double-release recorded\n * as a diagnostic no-op, immediate rejection of requests wider than\n * capacity, and a hard queue cap whose overflow surfaces as a structured\n * error code (§15.4 `queue_overflow`). Waiter timers stay ref'd — a permit\n * wait is actively awaited work — and are cleared on settle.\n */\n\nexport interface WorkloadPermitRequest {\n\treadonly requestId: string;\n\treadonly promptRunId: string;\n\treadonly workloadClass: WorkloadClass;\n\treadonly weight: 1 | 2;\n\treadonly signal?: AbortSignal;\n\treadonly timeoutMs?: number;\n}\n\nexport interface WorkloadPermit {\n\treadonly permitId: string;\n\treadonly requestId: string;\n\treadonly acquiredAt: string;\n\trelease(): void;\n}\n\nexport interface WorkloadPermitPoolSnapshot {\n\treadonly capacity: number;\n\treadonly activeWeight: number;\n\treadonly queuedCount: number;\n}\n\nexport type WorkloadPermitErrorCode = \"queue_overflow\" | \"timeout\" | \"aborted\" | \"over_capacity_weight\";\n\nexport class WorkloadPermitError extends Error {\n\treadonly code: WorkloadPermitErrorCode;\n\treadonly requestId: string;\n\n\tconstructor(code: WorkloadPermitErrorCode, requestId: string) {\n\t\tsuper(`workload permit ${code} (request ${requestId})`);\n\t\tthis.name = \"WorkloadPermitError\";\n\t\tthis.code = code;\n\t\tthis.requestId = requestId;\n\t}\n}\n\ninterface Waiter {\n\treadonly request: WorkloadPermitRequest;\n\treadonly resolve: (permit: WorkloadPermit) => void;\n\treadonly reject: (error: WorkloadPermitError) => void;\n\ttimer: ReturnType<typeof setTimeout> | null;\n\tabortListener: (() => void) | null;\n}\n\nexport const DEFAULT_PERMIT_QUEUE_CAP = 32;\n\nexport type WorkloadPermitPoolOptions = {\n\t/** Concurrent weight budget; §7.2 heavy caps feed this. Default 2. */\n\tcapacity?: number;\n\t/** §10.3 queue length hard cap. Default 32. */\n\tmaxQueue?: number;\n\t/** Injectable clock for deterministic tests. */\n\tnow?: () => Date;\n};\n\nexport class WorkloadPermitPool {\n\tprivate capacity: number;\n\tprivate readonly maxQueue: number;\n\tprivate readonly now: () => Date;\n\tprivate activeWeight = 0;\n\tprivate readonly queue: Waiter[] = [];\n\tprivate readonly releasedPermitIds = new Set<string>();\n\tprivate readonly activePermitIds = new Set<string>();\n\tprivate doubleReleases = 0;\n\n\tconstructor(options?: WorkloadPermitPoolOptions) {\n\t\tthis.capacity = sanitizeCapacity(options?.capacity, 2);\n\t\tthis.maxQueue = sanitizeCapacity(options?.maxQueue, DEFAULT_PERMIT_QUEUE_CAP);\n\t\tthis.now = options?.now ?? (() => new Date());\n\t}\n\n\t/**\n\t * Update capacity from a new admission decision. In-flight permits are\n\t * never revoked; a reduction only delays future grants until active\n\t * weight drains below the new capacity.\n\t */\n\tsetCapacity(capacity: number): void {\n\t\tthis.capacity = sanitizeCapacity(capacity, this.capacity);\n\t\tthis.grantWhilePossible();\n\t}\n\n\t/** Acquire a permit. Strict FIFO; rejects with {@link WorkloadPermitError}. */\n\tacquire(request: WorkloadPermitRequest): Promise<WorkloadPermit> {\n\t\tif (request.weight > this.capacity) {\n\t\t\treturn Promise.reject(new WorkloadPermitError(\"over_capacity_weight\", request.requestId));\n\t\t}\n\t\tif (request.signal?.aborted) {\n\t\t\treturn Promise.reject(new WorkloadPermitError(\"aborted\", request.requestId));\n\t\t}\n\t\tif (this.queue.length === 0 && this.activeWeight + request.weight <= this.capacity) {\n\t\t\treturn Promise.resolve(this.grant(request));\n\t\t}\n\t\tif (this.queue.length >= this.maxQueue) {\n\t\t\treturn Promise.reject(new WorkloadPermitError(\"queue_overflow\", request.requestId));\n\t\t}\n\t\treturn new Promise<WorkloadPermit>((resolve, reject) => {\n\t\t\tconst waiter: Waiter = { request, resolve, reject, timer: null, abortListener: null };\n\t\t\tif (typeof request.timeoutMs === \"number\" && Number.isFinite(request.timeoutMs) && request.timeoutMs > 0) {\n\t\t\t\twaiter.timer = setTimeout(() => {\n\t\t\t\t\tthis.evict(waiter, \"timeout\");\n\t\t\t\t}, request.timeoutMs);\n\t\t\t}\n\t\t\tif (request.signal) {\n\t\t\t\tconst signal = request.signal;\n\t\t\t\twaiter.abortListener = () => {\n\t\t\t\t\tthis.evict(waiter, \"aborted\");\n\t\t\t\t};\n\t\t\t\tsignal.addEventListener(\"abort\", waiter.abortListener, { once: true });\n\t\t\t}\n\t\t\tthis.queue.push(waiter);\n\t\t});\n\t}\n\n\tsnapshot(): WorkloadPermitPoolSnapshot {\n\t\treturn { capacity: this.capacity, activeWeight: this.activeWeight, queuedCount: this.queue.length };\n\t}\n\n\t/** Diagnostic count of double releases (§10.3 no-op + diagnostic). */\n\tget doubleReleaseCount(): number {\n\t\treturn this.doubleReleases;\n\t}\n\n\tprivate grant(request: WorkloadPermitRequest): WorkloadPermit {\n\t\tthis.activeWeight += request.weight;\n\t\tconst permitId = `permit-${randomUUID()}`;\n\t\tthis.activePermitIds.add(permitId);\n\t\tconst release = (): void => {\n\t\t\t// §10.3 exactly-once: a late or repeated settlement must not return\n\t\t\t// weight it no longer owns.\n\t\t\tif (this.releasedPermitIds.has(permitId) || !this.activePermitIds.has(permitId)) {\n\t\t\t\tthis.doubleReleases += 1;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.activePermitIds.delete(permitId);\n\t\t\tthis.releasedPermitIds.add(permitId);\n\t\t\tthis.activeWeight = Math.max(0, this.activeWeight - request.weight);\n\t\t\tthis.grantWhilePossible();\n\t\t};\n\t\treturn { permitId, requestId: request.requestId, acquiredAt: this.now().toISOString(), release };\n\t}\n\n\t/** Grant queued waiters strictly in FIFO order while the head fits (§10.4). */\n\tprivate grantWhilePossible(): void {\n\t\twhile (this.queue.length > 0) {\n\t\t\tconst head = this.queue[0];\n\t\t\tif (head.request.weight > this.capacity) {\n\t\t\t\t// Capacity shrank below the head's weight: reject rather than\n\t\t\t\t// starving everyone behind it forever.\n\t\t\t\tthis.queue.shift();\n\t\t\t\tthis.settleCleanup(head);\n\t\t\t\thead.reject(new WorkloadPermitError(\"over_capacity_weight\", head.request.requestId));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (this.activeWeight + head.request.weight > this.capacity) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.queue.shift();\n\t\t\tthis.settleCleanup(head);\n\t\t\thead.resolve(this.grant(head.request));\n\t\t}\n\t}\n\n\tprivate evict(waiter: Waiter, code: WorkloadPermitErrorCode): void {\n\t\tconst index = this.queue.indexOf(waiter);\n\t\tif (index === -1) {\n\t\t\treturn; // Already granted or already evicted.\n\t\t}\n\t\tthis.queue.splice(index, 1);\n\t\tthis.settleCleanup(waiter);\n\t\twaiter.reject(new WorkloadPermitError(code, waiter.request.requestId));\n\t}\n\n\tprivate settleCleanup(waiter: Waiter): void {\n\t\tif (waiter.timer !== null) {\n\t\t\tclearTimeout(waiter.timer);\n\t\t\twaiter.timer = null;\n\t\t}\n\t\tif (waiter.abortListener !== null) {\n\t\t\twaiter.request.signal?.removeEventListener(\"abort\", waiter.abortListener);\n\t\t\twaiter.abortListener = null;\n\t\t}\n\t}\n}\n\nfunction sanitizeCapacity(value: number | undefined, fallback: number): number {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 1) {\n\t\treturn fallback;\n\t}\n\treturn Math.floor(value);\n}\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ResourceAdmissionDecision } from "./resource-admission.ts";
|
|
2
|
+
import type { WorkloadPermitPool } from "./workload-permit-pool.ts";
|
|
3
|
+
import { type WorkloadShardPlan, type WorkloadShardSpec, type WorkloadShardState } from "./workload-shard-plan.ts";
|
|
4
|
+
import { type WorkloadShardStore } from "./workload-shard-store.ts";
|
|
5
|
+
/**
|
|
6
|
+
* Workload shard executor (OMK v0.97.x roadmap §13, M5 completion slice).
|
|
7
|
+
*
|
|
8
|
+
* Drives a validated `WorkloadShardPlan` through the journalled §13.4 state
|
|
9
|
+
* machine: passed shards are terminal and never re-run (§13.5 step 3),
|
|
10
|
+
* crash-orphaned `running` shards are recovered through `pending` re-arm
|
|
11
|
+
* records, concurrency is recomputed from the CURRENT admission decision
|
|
12
|
+
* (§13.5 step 6) plus the shared permit pool, and completion produces an
|
|
13
|
+
* aggregate evidence receipt (§13.6) — an observation input for
|
|
14
|
+
* `evaluateTask()`, never a task verdict by itself.
|
|
15
|
+
*
|
|
16
|
+
* Fail-closed rules (§21): a corrupt journal is quarantined and the run is
|
|
17
|
+
* `blocked`; a journal written for a different plan is `blocked`; an invalid
|
|
18
|
+
* plan never starts. The executor never rewrites shard commands (§12.3) —
|
|
19
|
+
* it only runs what the plan's descriptors already describe via the
|
|
20
|
+
* injected runner.
|
|
21
|
+
*/
|
|
22
|
+
export interface ShardRunContext {
|
|
23
|
+
readonly shard: WorkloadShardSpec;
|
|
24
|
+
readonly attempt: number;
|
|
25
|
+
readonly signal?: AbortSignal;
|
|
26
|
+
}
|
|
27
|
+
export type ShardRunner = (context: ShardRunContext) => Promise<{
|
|
28
|
+
readonly exitCode: number;
|
|
29
|
+
readonly evidenceRefs?: readonly string[];
|
|
30
|
+
}>;
|
|
31
|
+
export interface ExecuteWorkloadShardPlanInput {
|
|
32
|
+
readonly plan: WorkloadShardPlan;
|
|
33
|
+
readonly store: WorkloadShardStore;
|
|
34
|
+
readonly runner: ShardRunner;
|
|
35
|
+
readonly decision: ResourceAdmissionDecision;
|
|
36
|
+
readonly permitPool?: WorkloadPermitPool;
|
|
37
|
+
readonly signal?: AbortSignal;
|
|
38
|
+
/** §13.5 step 4 retry policy: re-arm previously failed shards. Default false. */
|
|
39
|
+
readonly retryFailed?: boolean;
|
|
40
|
+
/** §13.4: aborted -> pending needs an explicit resume; this call is that act. Default true. */
|
|
41
|
+
readonly resumeAborted?: boolean;
|
|
42
|
+
readonly permitWaitTimeoutMs?: number;
|
|
43
|
+
readonly now?: () => Date;
|
|
44
|
+
}
|
|
45
|
+
export interface ShardResultSummary {
|
|
46
|
+
readonly shardId: string;
|
|
47
|
+
readonly state: WorkloadShardState;
|
|
48
|
+
readonly attempt: number;
|
|
49
|
+
readonly evidenceRefs: readonly string[];
|
|
50
|
+
readonly reasonCode?: string;
|
|
51
|
+
}
|
|
52
|
+
/** §13.6 aggregate evidence receipt; §15.1 observation kind. */
|
|
53
|
+
export interface WorkloadShardAggregateEvidence {
|
|
54
|
+
readonly kind: "workload_shard_result.v1";
|
|
55
|
+
readonly planId: string;
|
|
56
|
+
readonly promptRunId: string;
|
|
57
|
+
readonly commandDigest: string;
|
|
58
|
+
readonly strategy: string;
|
|
59
|
+
readonly verdict: "passed" | "failed" | "aborted" | "blocked";
|
|
60
|
+
readonly reasonCodes: readonly string[];
|
|
61
|
+
readonly shardCount: number;
|
|
62
|
+
readonly counts: Readonly<Record<WorkloadShardState, number>>;
|
|
63
|
+
readonly shards: readonly ShardResultSummary[];
|
|
64
|
+
readonly effectiveConcurrency: number;
|
|
65
|
+
readonly completedAt: string;
|
|
66
|
+
}
|
|
67
|
+
export declare function executeWorkloadShardPlan(input: ExecuteWorkloadShardPlanInput): Promise<WorkloadShardAggregateEvidence>;
|
|
68
|
+
//# sourceMappingURL=workload-shard-executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workload-shard-executor.d.ts","sourceRoot":"","sources":["../../src/core/workload-shard-executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AACzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAIN,KAAK,iBAAiB,EAEtB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAA6B,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE/F;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC9B;AAED,MAAM,MAAM,WAAW,GAAG,CACzB,OAAO,EAAE,eAAe,KACpB,OAAO,CAAC;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC,CAAC;AAEvF,MAAM,WAAW,6BAA6B;IAC7C,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,yBAAyB,CAAC;IAC7C,QAAQ,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC;IACzC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,kFAAiF;IACjF,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,gGAA+F;IAC/F,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,kEAAgE;AAChE,MAAM,WAAW,8BAA8B;IAC9C,QAAQ,CAAC,IAAI,EAAE,0BAA0B,CAAC;IAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAC;IAC9D,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9D,QAAQ,CAAC,MAAM,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAC/C,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B;AAED,wBAAsB,wBAAwB,CAC7C,KAAK,EAAE,6BAA6B,GAClC,OAAO,CAAC,8BAA8B,CAAC,CAoFzC","sourcesContent":["import type { ResourceAdmissionDecision } from \"./resource-admission.ts\";\nimport type { WorkloadPermitPool } from \"./workload-permit-pool.ts\";\nimport {\n\tplanShardResume,\n\treduceShardRecords,\n\tvalidateWorkloadShardPlan,\n\ttype WorkloadShardPlan,\n\ttype WorkloadShardProjection,\n\ttype WorkloadShardSpec,\n\ttype WorkloadShardState,\n} from \"./workload-shard-plan.ts\";\nimport { applyTransition, runOneShard, shardById } from \"./workload-shard-runner.ts\";\nimport { WorkloadShardJournalError, type WorkloadShardStore } from \"./workload-shard-store.ts\";\n\n/**\n * Workload shard executor (OMK v0.97.x roadmap §13, M5 completion slice).\n *\n * Drives a validated `WorkloadShardPlan` through the journalled §13.4 state\n * machine: passed shards are terminal and never re-run (§13.5 step 3),\n * crash-orphaned `running` shards are recovered through `pending` re-arm\n * records, concurrency is recomputed from the CURRENT admission decision\n * (§13.5 step 6) plus the shared permit pool, and completion produces an\n * aggregate evidence receipt (§13.6) — an observation input for\n * `evaluateTask()`, never a task verdict by itself.\n *\n * Fail-closed rules (§21): a corrupt journal is quarantined and the run is\n * `blocked`; a journal written for a different plan is `blocked`; an invalid\n * plan never starts. The executor never rewrites shard commands (§12.3) —\n * it only runs what the plan's descriptors already describe via the\n * injected runner.\n */\n\nexport interface ShardRunContext {\n\treadonly shard: WorkloadShardSpec;\n\treadonly attempt: number;\n\treadonly signal?: AbortSignal;\n}\n\nexport type ShardRunner = (\n\tcontext: ShardRunContext,\n) => Promise<{ readonly exitCode: number; readonly evidenceRefs?: readonly string[] }>;\n\nexport interface ExecuteWorkloadShardPlanInput {\n\treadonly plan: WorkloadShardPlan;\n\treadonly store: WorkloadShardStore;\n\treadonly runner: ShardRunner;\n\treadonly decision: ResourceAdmissionDecision;\n\treadonly permitPool?: WorkloadPermitPool;\n\treadonly signal?: AbortSignal;\n\t/** §13.5 step 4 retry policy: re-arm previously failed shards. Default false. */\n\treadonly retryFailed?: boolean;\n\t/** §13.4: aborted -> pending needs an explicit resume; this call is that act. Default true. */\n\treadonly resumeAborted?: boolean;\n\treadonly permitWaitTimeoutMs?: number;\n\treadonly now?: () => Date;\n}\n\nexport interface ShardResultSummary {\n\treadonly shardId: string;\n\treadonly state: WorkloadShardState;\n\treadonly attempt: number;\n\treadonly evidenceRefs: readonly string[];\n\treadonly reasonCode?: string;\n}\n\n/** §13.6 aggregate evidence receipt; §15.1 observation kind. */\nexport interface WorkloadShardAggregateEvidence {\n\treadonly kind: \"workload_shard_result.v1\";\n\treadonly planId: string;\n\treadonly promptRunId: string;\n\treadonly commandDigest: string;\n\treadonly strategy: string;\n\treadonly verdict: \"passed\" | \"failed\" | \"aborted\" | \"blocked\";\n\treadonly reasonCodes: readonly string[];\n\treadonly shardCount: number;\n\treadonly counts: Readonly<Record<WorkloadShardState, number>>;\n\treadonly shards: readonly ShardResultSummary[];\n\treadonly effectiveConcurrency: number;\n\treadonly completedAt: string;\n}\n\nexport async function executeWorkloadShardPlan(\n\tinput: ExecuteWorkloadShardPlanInput,\n): Promise<WorkloadShardAggregateEvidence> {\n\tconst now = input.now ?? (() => new Date());\n\tconst planErrors = validateWorkloadShardPlan(input.plan);\n\tif (planErrors.length > 0) {\n\t\treturn blockedEvidence(input, now, [\"plan.invalid\", ...planErrors.slice(0, 3)]);\n\t}\n\n\tlet journal: ReturnType<WorkloadShardStore[\"load\"]>;\n\ttry {\n\t\tjournal = input.store.load();\n\t} catch (error) {\n\t\t// §21 fail closed: corrupt evidence is quarantined, never silently rebuilt.\n\t\tif (error instanceof WorkloadShardJournalError) {\n\t\t\tinput.store.quarantine();\n\t\t\treturn blockedEvidence(input, now, [`journal.${error.code}`]);\n\t\t}\n\t\tthrow error;\n\t}\n\tif (journal.plan === null) {\n\t\tinput.store.appendPlan(input.plan);\n\t} else if (journal.plan.planId !== input.plan.planId || journal.plan.commandDigest !== input.plan.commandDigest) {\n\t\treturn blockedEvidence(input, now, [\"plan.mismatch\"]);\n\t}\n\n\tconst projections = new Map<string, WorkloadShardProjection>(\n\t\tjournal.projections ?? reduceShardRecords(input.plan, []),\n\t);\n\tconst effectiveConcurrency = Math.max(1, Math.min(input.plan.maxConcurrency, input.decision.maxHeavyProcesses));\n\n\t// §13.5 steps 2-4: re-arm recoverable shards through explicit pending records.\n\tconst resume = planShardResume(input.plan, projections);\n\tconst rearm = [\n\t\t...resume.interrupted,\n\t\t...(input.retryFailed ? resume.retryable : []),\n\t\t...((input.resumeAborted ?? true) ? resume.resumable : []),\n\t];\n\tfor (const shardId of rearm) {\n\t\t// §13.5 step 2: a crash-orphaned `running` shard is first projected as\n\t\t// interrupted (running -> pending is illegal in §13.4), then re-armed.\n\t\tif (projections.get(shardId)?.state === \"running\") {\n\t\t\tapplyTransition(input, projections, shardId, \"interrupted\", { reasonCode: \"recovery.orphaned\", now });\n\t\t}\n\t\tapplyTransition(input, projections, shardId, \"pending\", { reasonCode: \"recovery.rearm\", now });\n\t}\n\n\t// Dependency-wave scheduler: launch ready shards up to the recomputed width.\n\tconst remaining = new Set(\n\t\tinput.plan.shards.map((shard) => shard.shardId).filter((id) => projections.get(id)?.state !== \"passed\"),\n\t);\n\tlet active = 0;\n\tlet wake: (() => void) | undefined;\n\tconst waitTurn = () =>\n\t\tnew Promise<void>((resolve) => {\n\t\t\twake = resolve;\n\t\t});\n\tconst running = new Map<string, Promise<void>>();\n\n\twhile (remaining.size > 0 && !input.signal?.aborted) {\n\t\tconst ready = [...remaining].filter((id) => {\n\t\t\tconst projection = projections.get(id);\n\t\t\tif (projection?.state !== \"pending\") return false;\n\t\t\tconst spec = shardById(input.plan, id);\n\t\t\treturn spec.dependencyIds.every((dep) => projections.get(dep)?.state === \"passed\");\n\t\t});\n\t\tconst launchable = ready.slice(0, Math.max(0, effectiveConcurrency - active));\n\t\tif (launchable.length === 0) {\n\t\t\tif (running.size === 0) break; // deadlock = blocked dependents; aggregate reports them\n\t\t\tawait Promise.race([...running.values(), waitTurn()]);\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const shardId of launchable) {\n\t\t\tremaining.delete(shardId);\n\t\t\tactive += 1;\n\t\t\tconst task = runOneShard(input, projections, shardId, now).finally(() => {\n\t\t\t\tactive -= 1;\n\t\t\t\trunning.delete(shardId);\n\t\t\t\twake?.();\n\t\t\t});\n\t\t\trunning.set(shardId, task);\n\t\t}\n\t}\n\tawait Promise.all(running.values());\n\n\treturn aggregate(input, projections, effectiveConcurrency, now, input.signal?.aborted ? [\"run.aborted\"] : []);\n}\n\nfunction aggregate(\n\tinput: ExecuteWorkloadShardPlanInput,\n\tprojections: ReadonlyMap<string, WorkloadShardProjection>,\n\teffectiveConcurrency: number,\n\tnow: () => Date,\n\textraReasons: readonly string[],\n): WorkloadShardAggregateEvidence {\n\tconst counts: Record<WorkloadShardState, number> = {\n\t\tpending: 0,\n\t\trunning: 0,\n\t\tpassed: 0,\n\t\tfailed: 0,\n\t\taborted: 0,\n\t\tinterrupted: 0,\n\t};\n\tconst shards: ShardResultSummary[] = input.plan.shards.map((shard) => {\n\t\tconst projection = projections.get(shard.shardId);\n\t\tconst state = projection?.state ?? \"pending\";\n\t\tcounts[state] += 1;\n\t\treturn {\n\t\t\tshardId: shard.shardId,\n\t\t\tstate,\n\t\t\tattempt: projection?.attempt ?? 0,\n\t\t\tevidenceRefs: projection?.evidenceRefs ?? [],\n\t\t\t...(projection?.reasonCode !== undefined ? { reasonCode: projection.reasonCode } : {}),\n\t\t};\n\t});\n\tconst verdict = aggregateVerdict(counts, input.plan.shards.length, extraReasons);\n\treturn {\n\t\tkind: \"workload_shard_result.v1\",\n\t\tplanId: input.plan.planId,\n\t\tpromptRunId: input.plan.promptRunId,\n\t\tcommandDigest: input.plan.commandDigest,\n\t\tstrategy: input.plan.strategy,\n\t\tverdict,\n\t\treasonCodes: extraReasons,\n\t\tshardCount: input.plan.shards.length,\n\t\tcounts,\n\t\tshards,\n\t\teffectiveConcurrency,\n\t\tcompletedAt: now().toISOString(),\n\t};\n}\n\nfunction blockedEvidence(\n\tinput: ExecuteWorkloadShardPlanInput,\n\tnow: () => Date,\n\treasonCodes: readonly string[],\n): WorkloadShardAggregateEvidence {\n\tconst counts: Record<WorkloadShardState, number> = {\n\t\tpending: input.plan.shards.length,\n\t\trunning: 0,\n\t\tpassed: 0,\n\t\tfailed: 0,\n\t\taborted: 0,\n\t\tinterrupted: 0,\n\t};\n\treturn {\n\t\tkind: \"workload_shard_result.v1\",\n\t\tplanId: input.plan.planId,\n\t\tpromptRunId: input.plan.promptRunId,\n\t\tcommandDigest: input.plan.commandDigest,\n\t\tstrategy: input.plan.strategy,\n\t\tverdict: \"blocked\",\n\t\treasonCodes,\n\t\tshardCount: input.plan.shards.length,\n\t\tcounts,\n\t\tshards: input.plan.shards.map((shard) => ({\n\t\t\tshardId: shard.shardId,\n\t\t\tstate: \"pending\" as const,\n\t\t\tattempt: 0,\n\t\t\tevidenceRefs: [],\n\t\t})),\n\t\teffectiveConcurrency: 0,\n\t\tcompletedAt: now().toISOString(),\n\t};\n}\n\n/** §13.6: \"passed\" requires EVERY shard to hold terminal passed evidence. */\nfunction aggregateVerdict(\n\tcounts: Readonly<Record<WorkloadShardState, number>>,\n\tshardCount: number,\n\textraReasons: readonly string[],\n): WorkloadShardAggregateEvidence[\"verdict\"] {\n\tif (counts.passed === shardCount) return \"passed\";\n\tif (counts.aborted > 0 || extraReasons.includes(\"run.aborted\")) return \"aborted\";\n\treturn \"failed\";\n}\n"]}
|