@wenathlan/saddle 1.8.17 → 2.0.2
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/Dockerfile +1339 -0
- package/README.md +68 -22
- package/alternatives.ts +1218 -0
- package/biome.json +153 -0
- package/boards.json +1517 -0
- package/compute.ts +3144 -0
- package/cores.json +1801 -0
- package/dist/acquisition.d.ts +359 -0
- package/dist/acquisition.d.ts.map +1 -0
- package/dist/acquisition.js +629 -0
- package/dist/acquisition.js.map +1 -0
- package/dist/alternatives.d.ts +509 -0
- package/dist/alternatives.d.ts.map +1 -0
- package/dist/alternatives.js +951 -0
- package/dist/alternatives.js.map +1 -0
- package/dist/automation.d.ts +426 -0
- package/dist/automation.d.ts.map +1 -0
- package/dist/automation.js +561 -0
- package/dist/automation.js.map +1 -0
- package/dist/browser.d.ts +562 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +1057 -0
- package/dist/browser.js.map +1 -0
- package/dist/capacitor.config.d.ts +13 -0
- package/dist/capacitor.config.d.ts.map +1 -0
- package/dist/capacitor.config.js +26 -0
- package/dist/capacitor.config.js.map +1 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +111 -0
- package/dist/cli.js.map +1 -0
- package/dist/communication.d.ts +192 -0
- package/dist/communication.d.ts.map +1 -0
- package/dist/communication.js +418 -0
- package/dist/communication.js.map +1 -0
- package/dist/compute.d.ts +1161 -0
- package/dist/compute.d.ts.map +1 -0
- package/dist/compute.js +2273 -0
- package/dist/compute.js.map +1 -0
- package/dist/distribution.d.ts +529 -0
- package/dist/distribution.d.ts.map +1 -0
- package/dist/distribution.js +811 -0
- package/dist/distribution.js.map +1 -0
- package/dist/execution.d.ts +530 -0
- package/dist/execution.d.ts.map +1 -0
- package/dist/execution.js +761 -0
- package/dist/execution.js.map +1 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +60 -0
- package/dist/format.js.map +1 -0
- package/dist/foundation.d.ts +176 -0
- package/dist/foundation.d.ts.map +1 -0
- package/dist/foundation.js +309 -0
- package/dist/foundation.js.map +1 -0
- package/dist/index.d.ts +733 -122
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1018 -122
- package/dist/index.js.map +1 -1
- package/dist/integration.d.ts +236 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +236 -0
- package/dist/integration.js.map +1 -0
- package/dist/intelligence.d.ts +94 -0
- package/dist/intelligence.d.ts.map +1 -0
- package/dist/intelligence.js +145 -0
- package/dist/intelligence.js.map +1 -0
- package/dist/isolation.d.ts +129 -0
- package/dist/isolation.d.ts.map +1 -0
- package/dist/isolation.js +96 -0
- package/dist/isolation.js.map +1 -0
- package/dist/media.d.ts +344 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +986 -0
- package/dist/media.js.map +1 -0
- package/dist/modes.d.ts +359 -0
- package/dist/modes.d.ts.map +1 -0
- package/dist/modes.js +134 -0
- package/dist/modes.js.map +1 -0
- package/dist/{library/public.d.ts → operations.d.ts} +15 -1
- package/dist/operations.d.ts.map +1 -0
- package/dist/{library/public.js → operations.js} +18 -7
- package/dist/operations.js.map +1 -0
- package/dist/orchestrator.d.ts +1339 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +4308 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/performance.d.ts +416 -0
- package/dist/performance.d.ts.map +1 -0
- package/dist/performance.js +827 -0
- package/dist/performance.js.map +1 -0
- package/dist/quantum.d.ts +810 -0
- package/dist/quantum.d.ts.map +1 -0
- package/dist/quantum.js +2073 -0
- package/dist/quantum.js.map +1 -0
- package/dist/render.d.ts +381 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +1073 -0
- package/dist/render.js.map +1 -0
- package/dist/scheduler.d.ts +596 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +1259 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/security.d.ts +501 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +1170 -0
- package/dist/security.js.map +1 -0
- package/dist/server.d.ts +21 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/{server/node.js → server.js} +16 -2
- package/dist/server.js.map +1 -0
- package/dist/tiers.d.ts +1002 -0
- package/dist/tiers.d.ts.map +1 -0
- package/dist/tiers.js +2165 -0
- package/dist/tiers.js.map +1 -0
- package/dist/virtual.d.ts +641 -0
- package/dist/virtual.d.ts.map +1 -0
- package/dist/virtual.js +1285 -0
- package/dist/virtual.js.map +1 -0
- package/dist/virtualcpu.d.ts +316 -0
- package/dist/virtualcpu.d.ts.map +1 -0
- package/dist/virtualcpu.js +1180 -0
- package/dist/virtualcpu.js.map +1 -0
- package/dist/virtualgpu.d.ts +345 -0
- package/dist/virtualgpu.d.ts.map +1 -0
- package/dist/virtualgpu.js +1000 -0
- package/dist/virtualgpu.js.map +1 -0
- package/dist/virtualization.d.ts +607 -0
- package/dist/virtualization.d.ts.map +1 -0
- package/dist/virtualization.js +1187 -0
- package/dist/virtualization.js.map +1 -0
- package/dist/virtualmemory.d.ts +818 -0
- package/dist/virtualmemory.d.ts.map +1 -0
- package/dist/virtualmemory.js +1796 -0
- package/dist/virtualmemory.js.map +1 -0
- package/dist/webscrape.d.ts +720 -0
- package/dist/webscrape.d.ts.map +1 -0
- package/dist/webscrape.js +2697 -0
- package/dist/webscrape.js.map +1 -0
- package/docker.config +769 -0
- package/docs/CONVERSA.txt +1523 -0
- package/docs/alternatives.md +499 -0
- package/docs/architecture-1.8.19.md +76 -0
- package/docs/architecture-1.8.19.mmd +33 -0
- package/docs/architecture-1.8.19.png +0 -0
- package/docs/architecture.md +355 -0
- package/docs/artifactavailability.md +6 -0
- package/docs/brancharchive-2026-08-18.md +18 -0
- package/docs/browser.md +7 -0
- package/docs/consolidation.md +70 -0
- package/docs/e2ugh-engine.md +528 -0
- package/{dist/examples/localjob.js → docs/example-localjob.ts} +3 -3
- package/{dist/examples/publicapi.js → docs/example-publicapi.ts} +2 -2
- package/docs/hardware.md +342 -0
- package/docs/optimization.md +126 -0
- package/docs/performance.md +499 -0
- package/docs/planning.1.8.18.md +94 -0
- package/docs/planning.1.8.19.md +49 -0
- package/docs/releasenotes-1.8.17.md +6 -0
- package/docs/releasenotes-1.8.18.md +32 -0
- package/docs/releasenotes-1.8.19.md +35 -0
- package/docs/research-1.8.18-isolation.md +43 -0
- package/docs/research-1.8.19-virtual-browser.md +27 -0
- package/docs/saddle.archive.1.8.17.tar.gz.gpg +0 -0
- package/docs/security.md +454 -0
- package/docs/todo-1.8.16.md +56 -1
- package/docs/todo-1.8.18.md +164 -0
- package/docs/todo-1.8.19.md +241 -0
- package/docs/viability.md +777 -0
- package/docs/virtualization.md +335 -0
- package/docs/web-duplicatecleanup.md +9 -0
- package/docs/web-ideas.md +106 -0
- package/docs/workflowimprovements-2026-08-19.md +70 -0
- package/gpumonitor.cpp +1543 -0
- package/gpus.json +2408 -0
- package/index.ts +1478 -0
- package/media.ts +1337 -0
- package/mttg.config +1549 -0
- package/orchestrator.ts +5462 -0
- package/package.json +304 -55
- package/passage.config +1331 -0
- package/performance.ts +1049 -0
- package/processors.json +2217 -0
- package/qemu.config +1062 -0
- package/qemubridge.py +1340 -0
- package/quantum.ts +2393 -0
- package/render.ts +1325 -0
- package/scheduler.ts +1600 -0
- package/security.ts +1449 -0
- package/tiers.ts +2871 -0
- package/tsconfig.json +29 -0
- package/virtualcpu.ts +1273 -0
- package/virtualgpu.ts +1181 -0
- package/virtualhardware.c +1011 -0
- package/virtualhardware.json +739 -0
- package/virtualization.ts +1628 -0
- package/virtualizationcore.cpp +4635 -0
- package/virtualmemory.ts +2303 -0
- package/vm.config.json +1910 -0
- package/{extension → web/extension}/manifest.json +1 -1
- package/web/readme.md +382 -0
- package/web/tsconfig.json +25 -0
- package/dist/adapters/forge.d.ts +0 -17
- package/dist/adapters/forge.d.ts.map +0 -1
- package/dist/adapters/forge.js +0 -19
- package/dist/adapters/forge.js.map +0 -1
- package/dist/adapters/forgejo.d.ts +0 -49
- package/dist/adapters/forgejo.d.ts.map +0 -1
- package/dist/adapters/forgejo.js +0 -8
- package/dist/adapters/forgejo.js.map +0 -1
- package/dist/adapters/github.d.ts +0 -12
- package/dist/adapters/github.d.ts.map +0 -1
- package/dist/adapters/github.js +0 -20
- package/dist/adapters/github.js.map +0 -1
- package/dist/adapters/gitlab.d.ts +0 -17
- package/dist/adapters/gitlab.d.ts.map +0 -1
- package/dist/adapters/gitlab.js +0 -10
- package/dist/adapters/gitlab.js.map +0 -1
- package/dist/adapters/huggingface.d.ts +0 -17
- package/dist/adapters/huggingface.d.ts.map +0 -1
- package/dist/adapters/huggingface.js +0 -6
- package/dist/adapters/huggingface.js.map +0 -1
- package/dist/adapters/socket.d.ts +0 -11
- package/dist/adapters/socket.d.ts.map +0 -1
- package/dist/adapters/socket.js +0 -17
- package/dist/adapters/socket.js.map +0 -1
- package/dist/adapters/transport.d.ts +0 -7
- package/dist/adapters/transport.d.ts.map +0 -1
- package/dist/adapters/transport.js +0 -36
- package/dist/adapters/transport.js.map +0 -1
- package/dist/ai/chunk.d.ts +0 -2
- package/dist/ai/chunk.d.ts.map +0 -1
- package/dist/ai/chunk.js +0 -36
- package/dist/ai/chunk.js.map +0 -1
- package/dist/ai/llmstxt.d.ts +0 -6
- package/dist/ai/llmstxt.d.ts.map +0 -1
- package/dist/ai/llmstxt.js +0 -12
- package/dist/ai/llmstxt.js.map +0 -1
- package/dist/ai/provenance.d.ts +0 -22
- package/dist/ai/provenance.d.ts.map +0 -1
- package/dist/ai/provenance.js +0 -25
- package/dist/ai/provenance.js.map +0 -1
- package/dist/ai/rag.d.ts +0 -26
- package/dist/ai/rag.d.ts.map +0 -1
- package/dist/ai/rag.js +0 -22
- package/dist/ai/rag.js.map +0 -1
- package/dist/ai/tokens.d.ts +0 -15
- package/dist/ai/tokens.d.ts.map +0 -1
- package/dist/ai/tokens.js +0 -9
- package/dist/ai/tokens.js.map +0 -1
- package/dist/api/auth.d.ts +0 -16
- package/dist/api/auth.d.ts.map +0 -1
- package/dist/api/auth.js +0 -19
- package/dist/api/auth.js.map +0 -1
- package/dist/api/contracts.d.ts +0 -28
- package/dist/api/contracts.d.ts.map +0 -1
- package/dist/api/contracts.js +0 -14
- package/dist/api/contracts.js.map +0 -1
- package/dist/api/control.d.ts +0 -10
- package/dist/api/control.d.ts.map +0 -1
- package/dist/api/control.js +0 -35
- package/dist/api/control.js.map +0 -1
- package/dist/api/http.d.ts +0 -7
- package/dist/api/http.d.ts.map +0 -1
- package/dist/api/http.js +0 -12
- package/dist/api/http.js.map +0 -1
- package/dist/api/rate.d.ts +0 -28
- package/dist/api/rate.d.ts.map +0 -1
- package/dist/api/rate.js +0 -36
- package/dist/api/rate.js.map +0 -1
- package/dist/api/security.d.ts +0 -12
- package/dist/api/security.d.ts.map +0 -1
- package/dist/api/security.js +0 -56
- package/dist/api/security.js.map +0 -1
- package/dist/api/service.d.ts +0 -5
- package/dist/api/service.d.ts.map +0 -1
- package/dist/api/service.js +0 -70
- package/dist/api/service.js.map +0 -1
- package/dist/apps/registry.d.ts +0 -33
- package/dist/apps/registry.d.ts.map +0 -1
- package/dist/apps/registry.js +0 -24
- package/dist/apps/registry.js.map +0 -1
- package/dist/binary/archive.d.ts +0 -27
- package/dist/binary/archive.d.ts.map +0 -1
- package/dist/binary/archive.js +0 -46
- package/dist/binary/archive.js.map +0 -1
- package/dist/binary/build.d.ts +0 -29
- package/dist/binary/build.d.ts.map +0 -1
- package/dist/binary/build.js +0 -17
- package/dist/binary/build.js.map +0 -1
- package/dist/binary/transform.d.ts +0 -70
- package/dist/binary/transform.d.ts.map +0 -1
- package/dist/binary/transform.js +0 -105
- package/dist/binary/transform.js.map +0 -1
- package/dist/bot/adapter.d.ts +0 -5
- package/dist/bot/adapter.d.ts.map +0 -1
- package/dist/bot/adapter.js +0 -11
- package/dist/bot/adapter.js.map +0 -1
- package/dist/bot/bot.d.ts +0 -28
- package/dist/bot/bot.d.ts.map +0 -1
- package/dist/bot/bot.js +0 -52
- package/dist/bot/bot.js.map +0 -1
- package/dist/bot/commands.d.ts +0 -8
- package/dist/bot/commands.d.ts.map +0 -1
- package/dist/bot/commands.js +0 -19
- package/dist/bot/commands.js.map +0 -1
- package/dist/bot/permissions.d.ts +0 -13
- package/dist/bot/permissions.d.ts.map +0 -1
- package/dist/bot/permissions.js +0 -16
- package/dist/bot/permissions.js.map +0 -1
- package/dist/browser/actions.d.ts +0 -33
- package/dist/browser/actions.d.ts.map +0 -1
- package/dist/browser/actions.js +0 -44
- package/dist/browser/actions.js.map +0 -1
- package/dist/browser/agent.d.ts +0 -21
- package/dist/browser/agent.d.ts.map +0 -1
- package/dist/browser/agent.js +0 -13
- package/dist/browser/agent.js.map +0 -1
- package/dist/browser/context.d.ts +0 -43
- package/dist/browser/context.d.ts.map +0 -1
- package/dist/browser/context.js +0 -54
- package/dist/browser/context.js.map +0 -1
- package/dist/browser/fingerprint.d.ts +0 -4
- package/dist/browser/fingerprint.d.ts.map +0 -1
- package/dist/browser/fingerprint.js +0 -12
- package/dist/browser/fingerprint.js.map +0 -1
- package/dist/browser/index.d.ts +0 -11
- package/dist/browser/index.d.ts.map +0 -1
- package/dist/browser/index.js +0 -11
- package/dist/browser/index.js.map +0 -1
- package/dist/browser/playwright.d.ts +0 -11
- package/dist/browser/playwright.d.ts.map +0 -1
- package/dist/browser/playwright.js +0 -24
- package/dist/browser/playwright.js.map +0 -1
- package/dist/browser/recorder.d.ts +0 -21
- package/dist/browser/recorder.d.ts.map +0 -1
- package/dist/browser/recorder.js +0 -58
- package/dist/browser/recorder.js.map +0 -1
- package/dist/browser/session.d.ts +0 -14
- package/dist/browser/session.d.ts.map +0 -1
- package/dist/browser/session.js +0 -21
- package/dist/browser/session.js.map +0 -1
- package/dist/browser/snapshot.d.ts +0 -69
- package/dist/browser/snapshot.d.ts.map +0 -1
- package/dist/browser/snapshot.js +0 -133
- package/dist/browser/snapshot.js.map +0 -1
- package/dist/captcha/contract.d.ts +0 -35
- package/dist/captcha/contract.d.ts.map +0 -1
- package/dist/captcha/contract.js +0 -17
- package/dist/captcha/contract.js.map +0 -1
- package/dist/captcha/evidence.d.ts +0 -10
- package/dist/captcha/evidence.d.ts.map +0 -1
- package/dist/captcha/evidence.js +0 -9
- package/dist/captcha/evidence.js.map +0 -1
- package/dist/captcha/guard.d.ts +0 -23
- package/dist/captcha/guard.d.ts.map +0 -1
- package/dist/captcha/guard.js +0 -12
- package/dist/captcha/guard.js.map +0 -1
- package/dist/cli/main.d.ts +0 -4
- package/dist/cli/main.d.ts.map +0 -1
- package/dist/cli/main.js +0 -46
- package/dist/cli/main.js.map +0 -1
- package/dist/core/errors.d.ts +0 -84
- package/dist/core/errors.d.ts.map +0 -1
- package/dist/core/errors.js +0 -54
- package/dist/core/errors.js.map +0 -1
- package/dist/core/events.d.ts +0 -9
- package/dist/core/events.d.ts.map +0 -1
- package/dist/core/events.js +0 -21
- package/dist/core/events.js.map +0 -1
- package/dist/core/hash.d.ts +0 -10
- package/dist/core/hash.d.ts.map +0 -1
- package/dist/core/hash.js +0 -82
- package/dist/core/hash.js.map +0 -1
- package/dist/core/ids.d.ts +0 -10
- package/dist/core/ids.d.ts.map +0 -1
- package/dist/core/ids.js +0 -15
- package/dist/core/ids.js.map +0 -1
- package/dist/delivery/manifest.d.ts +0 -35
- package/dist/delivery/manifest.d.ts.map +0 -1
- package/dist/delivery/manifest.js +0 -68
- package/dist/delivery/manifest.js.map +0 -1
- package/dist/deploy/index.d.ts +0 -6
- package/dist/deploy/index.d.ts.map +0 -1
- package/dist/deploy/index.js +0 -6
- package/dist/deploy/index.js.map +0 -1
- package/dist/dispatch/resumable.d.ts +0 -102
- package/dist/dispatch/resumable.d.ts.map +0 -1
- package/dist/dispatch/resumable.js +0 -64
- package/dist/dispatch/resumable.js.map +0 -1
- package/dist/dispatch/workflow.d.ts +0 -6
- package/dist/dispatch/workflow.d.ts.map +0 -1
- package/dist/dispatch/workflow.js +0 -37
- package/dist/dispatch/workflow.js.map +0 -1
- package/dist/domain/artifacts.d.ts +0 -12
- package/dist/domain/artifacts.d.ts.map +0 -1
- package/dist/domain/artifacts.js +0 -14
- package/dist/domain/artifacts.js.map +0 -1
- package/dist/domain/jobs.d.ts +0 -12
- package/dist/domain/jobs.d.ts.map +0 -1
- package/dist/domain/jobs.js +0 -20
- package/dist/domain/jobs.js.map +0 -1
- package/dist/domain/providers.d.ts +0 -10
- package/dist/domain/providers.d.ts.map +0 -1
- package/dist/domain/providers.js +0 -8
- package/dist/domain/providers.js.map +0 -1
- package/dist/domain/runtime.d.ts +0 -14
- package/dist/domain/runtime.d.ts.map +0 -1
- package/dist/domain/runtime.js +0 -10
- package/dist/domain/runtime.js.map +0 -1
- package/dist/domain/sessions.d.ts +0 -12
- package/dist/domain/sessions.d.ts.map +0 -1
- package/dist/domain/sessions.js +0 -57
- package/dist/domain/sessions.js.map +0 -1
- package/dist/examples/localjob.d.ts +0 -2
- package/dist/examples/localjob.d.ts.map +0 -1
- package/dist/examples/localjob.js.map +0 -1
- package/dist/examples/publicapi.d.ts +0 -2
- package/dist/examples/publicapi.d.ts.map +0 -1
- package/dist/examples/publicapi.js.map +0 -1
- package/dist/extension/build.d.ts +0 -9
- package/dist/extension/build.d.ts.map +0 -1
- package/dist/extension/build.js +0 -85
- package/dist/extension/build.js.map +0 -1
- package/dist/extension/content.d.ts +0 -6
- package/dist/extension/content.d.ts.map +0 -1
- package/dist/extension/content.js +0 -135
- package/dist/extension/content.js.map +0 -1
- package/dist/extension/index.d.ts +0 -7
- package/dist/extension/index.d.ts.map +0 -1
- package/dist/extension/index.js +0 -7
- package/dist/extension/index.js.map +0 -1
- package/dist/extension/pagebridge.d.ts +0 -6
- package/dist/extension/pagebridge.d.ts.map +0 -1
- package/dist/extension/pagebridge.js +0 -42
- package/dist/extension/pagebridge.js.map +0 -1
- package/dist/extension/permissions.d.ts +0 -26
- package/dist/extension/permissions.d.ts.map +0 -1
- package/dist/extension/permissions.js +0 -28
- package/dist/extension/permissions.js.map +0 -1
- package/dist/extension/popup.d.ts +0 -5
- package/dist/extension/popup.d.ts.map +0 -1
- package/dist/extension/popup.js +0 -26
- package/dist/extension/popup.js.map +0 -1
- package/dist/extension/protocol.d.ts +0 -58
- package/dist/extension/protocol.d.ts.map +0 -1
- package/dist/extension/protocol.js +0 -101
- package/dist/extension/protocol.js.map +0 -1
- package/dist/extension/serviceworker.d.ts +0 -25
- package/dist/extension/serviceworker.d.ts.map +0 -1
- package/dist/extension/serviceworker.js +0 -102
- package/dist/extension/serviceworker.js.map +0 -1
- package/dist/extension/worker.d.ts +0 -28
- package/dist/extension/worker.d.ts.map +0 -1
- package/dist/extension/worker.js +0 -22
- package/dist/extension/worker.js.map +0 -1
- package/dist/format/check.d.ts +0 -8
- package/dist/format/check.d.ts.map +0 -1
- package/dist/format/check.js +0 -38
- package/dist/format/check.js.map +0 -1
- package/dist/library/public.d.ts.map +0 -1
- package/dist/library/public.js.map +0 -1
- package/dist/mcp/browser.d.ts +0 -9
- package/dist/mcp/browser.d.ts.map +0 -1
- package/dist/mcp/browser.js +0 -13
- package/dist/mcp/browser.js.map +0 -1
- package/dist/mcp/server.d.ts +0 -35
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js +0 -40
- package/dist/mcp/server.js.map +0 -1
- package/dist/mcp/transport.d.ts +0 -7
- package/dist/mcp/transport.d.ts.map +0 -1
- package/dist/mcp/transport.js +0 -17
- package/dist/mcp/transport.js.map +0 -1
- package/dist/memory/bridge.d.ts +0 -14
- package/dist/memory/bridge.d.ts.map +0 -1
- package/dist/memory/bridge.js +0 -16
- package/dist/memory/bridge.js.map +0 -1
- package/dist/memory/engine.d.ts +0 -38
- package/dist/memory/engine.d.ts.map +0 -1
- package/dist/memory/engine.js +0 -109
- package/dist/memory/engine.js.map +0 -1
- package/dist/memory/modes.d.ts +0 -43
- package/dist/memory/modes.d.ts.map +0 -1
- package/dist/memory/modes.js +0 -66
- package/dist/memory/modes.js.map +0 -1
- package/dist/memory/objects.d.ts +0 -21
- package/dist/memory/objects.d.ts.map +0 -1
- package/dist/memory/objects.js +0 -20
- package/dist/memory/objects.js.map +0 -1
- package/dist/memory/planner.d.ts +0 -66
- package/dist/memory/planner.d.ts.map +0 -1
- package/dist/memory/planner.js +0 -108
- package/dist/memory/planner.js.map +0 -1
- package/dist/memory/targets.d.ts +0 -78
- package/dist/memory/targets.d.ts.map +0 -1
- package/dist/memory/targets.js +0 -29
- package/dist/memory/targets.js.map +0 -1
- package/dist/memory/transforms.d.ts +0 -19
- package/dist/memory/transforms.d.ts.map +0 -1
- package/dist/memory/transforms.js +0 -14
- package/dist/memory/transforms.js.map +0 -1
- package/dist/modes/matrix.d.ts +0 -23
- package/dist/modes/matrix.d.ts.map +0 -1
- package/dist/modes/matrix.js +0 -18
- package/dist/modes/matrix.js.map +0 -1
- package/dist/modes/modes.d.ts +0 -33
- package/dist/modes/modes.d.ts.map +0 -1
- package/dist/modes/modes.js +0 -15
- package/dist/modes/modes.js.map +0 -1
- package/dist/modes/resolve.d.ts +0 -96
- package/dist/modes/resolve.d.ts.map +0 -1
- package/dist/modes/resolve.js +0 -48
- package/dist/modes/resolve.js.map +0 -1
- package/dist/observability/metrics.d.ts +0 -14
- package/dist/observability/metrics.d.ts.map +0 -1
- package/dist/observability/metrics.js +0 -18
- package/dist/observability/metrics.js.map +0 -1
- package/dist/packager/manifest.d.ts +0 -66
- package/dist/packager/manifest.d.ts.map +0 -1
- package/dist/packager/manifest.js +0 -94
- package/dist/packager/manifest.js.map +0 -1
- package/dist/packager/publish.d.ts +0 -51
- package/dist/packager/publish.d.ts.map +0 -1
- package/dist/packager/publish.js +0 -18
- package/dist/packager/publish.js.map +0 -1
- package/dist/packager/targetcli.d.ts +0 -7
- package/dist/packager/targetcli.d.ts.map +0 -1
- package/dist/packager/targetcli.js +0 -22
- package/dist/packager/targetcli.js.map +0 -1
- package/dist/persistence/adapter.d.ts +0 -5
- package/dist/persistence/adapter.d.ts.map +0 -1
- package/dist/persistence/adapter.js +0 -11
- package/dist/persistence/adapter.js.map +0 -1
- package/dist/persistence/drizzle.d.ts +0 -2
- package/dist/persistence/drizzle.d.ts.map +0 -1
- package/dist/persistence/drizzle.js +0 -12
- package/dist/persistence/drizzle.js.map +0 -1
- package/dist/persistence/memory.d.ts +0 -2
- package/dist/persistence/memory.d.ts.map +0 -1
- package/dist/persistence/memory.js +0 -27
- package/dist/persistence/memory.js.map +0 -1
- package/dist/persistence/migrations.d.ts +0 -19
- package/dist/persistence/migrations.d.ts.map +0 -1
- package/dist/persistence/migrations.js +0 -13
- package/dist/persistence/migrations.js.map +0 -1
- package/dist/persistence/prisma.d.ts +0 -2
- package/dist/persistence/prisma.d.ts.map +0 -1
- package/dist/persistence/prisma.js +0 -25
- package/dist/persistence/prisma.js.map +0 -1
- package/dist/persistence/schema.d.ts +0 -63
- package/dist/persistence/schema.d.ts.map +0 -1
- package/dist/persistence/schema.js +0 -28
- package/dist/persistence/schema.js.map +0 -1
- package/dist/persistence/sql.d.ts +0 -3
- package/dist/persistence/sql.d.ts.map +0 -1
- package/dist/persistence/sql.js +0 -37
- package/dist/persistence/sql.js.map +0 -1
- package/dist/protocol/blocks.d.ts +0 -10
- package/dist/protocol/blocks.d.ts.map +0 -1
- package/dist/protocol/blocks.js +0 -26
- package/dist/protocol/blocks.js.map +0 -1
- package/dist/protocol/json.d.ts +0 -6
- package/dist/protocol/json.d.ts.map +0 -1
- package/dist/protocol/json.js +0 -6
- package/dist/protocol/json.js.map +0 -1
- package/dist/protocol/ndjson.d.ts +0 -3
- package/dist/protocol/ndjson.d.ts.map +0 -1
- package/dist/protocol/ndjson.js +0 -20
- package/dist/protocol/ndjson.js.map +0 -1
- package/dist/protocol/sse.d.ts +0 -7
- package/dist/protocol/sse.d.ts.map +0 -1
- package/dist/protocol/sse.js +0 -25
- package/dist/protocol/sse.js.map +0 -1
- package/dist/proxy/pool.d.ts +0 -9
- package/dist/proxy/pool.d.ts.map +0 -1
- package/dist/proxy/pool.js +0 -27
- package/dist/proxy/pool.js.map +0 -1
- package/dist/queue/idempotency.d.ts +0 -10
- package/dist/queue/idempotency.d.ts.map +0 -1
- package/dist/queue/idempotency.js +0 -13
- package/dist/queue/idempotency.js.map +0 -1
- package/dist/queue/persistent.d.ts +0 -14
- package/dist/queue/persistent.d.ts.map +0 -1
- package/dist/queue/persistent.js +0 -66
- package/dist/queue/persistent.js.map +0 -1
- package/dist/queue/queue.d.ts +0 -7
- package/dist/queue/queue.d.ts.map +0 -1
- package/dist/queue/queue.js +0 -55
- package/dist/queue/queue.js.map +0 -1
- package/dist/queue/saga.d.ts +0 -5
- package/dist/queue/saga.d.ts.map +0 -1
- package/dist/queue/saga.js +0 -20
- package/dist/queue/saga.js.map +0 -1
- package/dist/release/assets.d.ts +0 -144
- package/dist/release/assets.d.ts.map +0 -1
- package/dist/release/assets.js +0 -157
- package/dist/release/assets.js.map +0 -1
- package/dist/release/evidence.d.ts +0 -92
- package/dist/release/evidence.d.ts.map +0 -1
- package/dist/release/evidence.js +0 -148
- package/dist/release/evidence.js.map +0 -1
- package/dist/release/verify.d.ts +0 -12
- package/dist/release/verify.d.ts.map +0 -1
- package/dist/release/verify.js +0 -112
- package/dist/release/verify.js.map +0 -1
- package/dist/runners/chain.d.ts +0 -125
- package/dist/runners/chain.d.ts.map +0 -1
- package/dist/runners/chain.js +0 -95
- package/dist/runners/chain.js.map +0 -1
- package/dist/runners/health.d.ts +0 -36
- package/dist/runners/health.d.ts.map +0 -1
- package/dist/runners/health.js +0 -26
- package/dist/runners/health.js.map +0 -1
- package/dist/runners/heartbeat.d.ts +0 -23
- package/dist/runners/heartbeat.d.ts.map +0 -1
- package/dist/runners/heartbeat.js +0 -29
- package/dist/runners/heartbeat.js.map +0 -1
- package/dist/runners/inprocess.d.ts +0 -17
- package/dist/runners/inprocess.d.ts.map +0 -1
- package/dist/runners/inprocess.js +0 -20
- package/dist/runners/inprocess.js.map +0 -1
- package/dist/runners/scheduler.d.ts +0 -5
- package/dist/runners/scheduler.d.ts.map +0 -1
- package/dist/runners/scheduler.js +0 -19
- package/dist/runners/scheduler.js.map +0 -1
- package/dist/runtime/abort.d.ts +0 -8
- package/dist/runtime/abort.d.ts.map +0 -1
- package/dist/runtime/abort.js +0 -13
- package/dist/runtime/abort.js.map +0 -1
- package/dist/runtime/compatibility.d.ts +0 -37
- package/dist/runtime/compatibility.d.ts.map +0 -1
- package/dist/runtime/compatibility.js +0 -10
- package/dist/runtime/compatibility.js.map +0 -1
- package/dist/runtime/detect.d.ts +0 -13
- package/dist/runtime/detect.d.ts.map +0 -1
- package/dist/runtime/detect.js +0 -18
- package/dist/runtime/detect.js.map +0 -1
- package/dist/runtime/engine.d.ts +0 -20
- package/dist/runtime/engine.d.ts.map +0 -1
- package/dist/runtime/engine.js +0 -61
- package/dist/runtime/engine.js.map +0 -1
- package/dist/runtime/retry.d.ts +0 -19
- package/dist/runtime/retry.d.ts.map +0 -1
- package/dist/runtime/retry.js +0 -54
- package/dist/runtime/retry.js.map +0 -1
- package/dist/runtime/worker.d.ts +0 -10
- package/dist/runtime/worker.d.ts.map +0 -1
- package/dist/runtime/worker.js +0 -25
- package/dist/runtime/worker.js.map +0 -1
- package/dist/scrape/cache.d.ts +0 -45
- package/dist/scrape/cache.d.ts.map +0 -1
- package/dist/scrape/cache.js +0 -45
- package/dist/scrape/cache.js.map +0 -1
- package/dist/scrape/crawl.d.ts +0 -47
- package/dist/scrape/crawl.d.ts.map +0 -1
- package/dist/scrape/crawl.js +0 -114
- package/dist/scrape/crawl.js.map +0 -1
- package/dist/scrape/extract.d.ts +0 -63
- package/dist/scrape/extract.d.ts.map +0 -1
- package/dist/scrape/extract.js +0 -47
- package/dist/scrape/extract.js.map +0 -1
- package/dist/scrape/normalize.d.ts +0 -52
- package/dist/scrape/normalize.d.ts.map +0 -1
- package/dist/scrape/normalize.js +0 -102
- package/dist/scrape/normalize.js.map +0 -1
- package/dist/scrape/robots.d.ts +0 -37
- package/dist/scrape/robots.d.ts.map +0 -1
- package/dist/scrape/robots.js +0 -71
- package/dist/scrape/robots.js.map +0 -1
- package/dist/scrape/schema.d.ts +0 -23
- package/dist/scrape/schema.d.ts.map +0 -1
- package/dist/scrape/schema.js +0 -92
- package/dist/scrape/schema.js.map +0 -1
- package/dist/scrape/scraper.d.ts +0 -6
- package/dist/scrape/scraper.d.ts.map +0 -1
- package/dist/scrape/scraper.js +0 -46
- package/dist/scrape/scraper.js.map +0 -1
- package/dist/scrape/semantic.d.ts +0 -25
- package/dist/scrape/semantic.d.ts.map +0 -1
- package/dist/scrape/semantic.js +0 -27
- package/dist/scrape/semantic.js.map +0 -1
- package/dist/server/node.d.ts +0 -7
- package/dist/server/node.d.ts.map +0 -1
- package/dist/server/node.js.map +0 -1
- package/dist/sessions/file.d.ts +0 -25
- package/dist/sessions/file.d.ts.map +0 -1
- package/dist/sessions/file.js +0 -13
- package/dist/sessions/file.js.map +0 -1
- package/dist/sessions/replay.d.ts +0 -9
- package/dist/sessions/replay.d.ts.map +0 -1
- package/dist/sessions/replay.js +0 -70
- package/dist/sessions/replay.js.map +0 -1
- package/dist/sessions/store.d.ts +0 -25
- package/dist/sessions/store.d.ts.map +0 -1
- package/dist/sessions/store.js +0 -13
- package/dist/sessions/store.js.map +0 -1
- package/dist/storage/adapter.d.ts +0 -5
- package/dist/storage/adapter.d.ts.map +0 -1
- package/dist/storage/adapter.js +0 -11
- package/dist/storage/adapter.js.map +0 -1
- package/dist/storage/cache.d.ts +0 -23
- package/dist/storage/cache.d.ts.map +0 -1
- package/dist/storage/cache.js +0 -79
- package/dist/storage/cache.js.map +0 -1
- package/dist/storage/checksum.d.ts +0 -3
- package/dist/storage/checksum.d.ts.map +0 -1
- package/dist/storage/checksum.js +0 -19
- package/dist/storage/checksum.js.map +0 -1
- package/dist/storage/chunked.d.ts +0 -21
- package/dist/storage/chunked.d.ts.map +0 -1
- package/dist/storage/chunked.js +0 -67
- package/dist/storage/chunked.js.map +0 -1
- package/dist/storage/content.d.ts +0 -24
- package/dist/storage/content.d.ts.map +0 -1
- package/dist/storage/content.js +0 -44
- package/dist/storage/content.js.map +0 -1
- package/dist/storage/filehosting.d.ts +0 -2
- package/dist/storage/filehosting.d.ts.map +0 -1
- package/dist/storage/filehosting.js +0 -24
- package/dist/storage/filehosting.js.map +0 -1
- package/dist/storage/githubcontents.d.ts +0 -2
- package/dist/storage/githubcontents.d.ts.map +0 -1
- package/dist/storage/githubcontents.js +0 -29
- package/dist/storage/githubcontents.js.map +0 -1
- package/dist/storage/index.d.ts +0 -12
- package/dist/storage/index.d.ts.map +0 -1
- package/dist/storage/index.js +0 -12
- package/dist/storage/index.js.map +0 -1
- package/dist/storage/local.d.ts +0 -2
- package/dist/storage/local.d.ts.map +0 -1
- package/dist/storage/local.js +0 -72
- package/dist/storage/local.js.map +0 -1
- package/dist/storage/memory.d.ts +0 -6
- package/dist/storage/memory.d.ts.map +0 -1
- package/dist/storage/memory.js +0 -36
- package/dist/storage/memory.js.map +0 -1
- package/dist/storage/pool.d.ts +0 -95
- package/dist/storage/pool.d.ts.map +0 -1
- package/dist/storage/pool.js +0 -202
- package/dist/storage/pool.js.map +0 -1
- package/dist/storage/s3compatible.d.ts +0 -3
- package/dist/storage/s3compatible.d.ts.map +0 -1
- package/dist/storage/s3compatible.js +0 -70
- package/dist/storage/s3compatible.js.map +0 -1
- package/dist/storage/sync.d.ts +0 -47
- package/dist/storage/sync.d.ts.map +0 -1
- package/dist/storage/sync.js +0 -75
- package/dist/storage/sync.js.map +0 -1
- package/dist/surfaces/adapters.d.ts +0 -97
- package/dist/surfaces/adapters.d.ts.map +0 -1
- package/dist/surfaces/adapters.js +0 -50
- package/dist/surfaces/adapters.js.map +0 -1
- package/dist/surfaces/controls.d.ts +0 -18
- package/dist/surfaces/controls.d.ts.map +0 -1
- package/dist/surfaces/controls.js +0 -45
- package/dist/surfaces/controls.js.map +0 -1
- package/dist/surfaces/manifest.d.ts +0 -68
- package/dist/surfaces/manifest.d.ts.map +0 -1
- package/dist/surfaces/manifest.js +0 -42
- package/dist/surfaces/manifest.js.map +0 -1
- package/dist/surfaces/n8n.d.ts +0 -25
- package/dist/surfaces/n8n.d.ts.map +0 -1
- package/dist/surfaces/n8n.js +0 -31
- package/dist/surfaces/n8n.js.map +0 -1
- package/dist/surfaces/operations.d.ts +0 -32
- package/dist/surfaces/operations.d.ts.map +0 -1
- package/dist/surfaces/operations.js +0 -53
- package/dist/surfaces/operations.js.map +0 -1
- package/dist/surfaces/requirements.d.ts +0 -29
- package/dist/surfaces/requirements.d.ts.map +0 -1
- package/dist/surfaces/requirements.js +0 -50
- package/dist/surfaces/requirements.js.map +0 -1
- package/dist/surfaces/targets.d.ts +0 -196
- package/dist/surfaces/targets.d.ts.map +0 -1
- package/dist/surfaces/targets.js +0 -27
- package/dist/surfaces/targets.js.map +0 -1
- package/dist/webhook/delivery.d.ts +0 -18
- package/dist/webhook/delivery.d.ts.map +0 -1
- package/dist/webhook/delivery.js +0 -36
- package/dist/webhook/delivery.js.map +0 -1
- package/dist/webhook/receiver.d.ts +0 -26
- package/dist/webhook/receiver.d.ts.map +0 -1
- package/dist/webhook/receiver.js +0 -24
- package/dist/webhook/receiver.js.map +0 -1
- package/dist/webhook/signature.d.ts +0 -3
- package/dist/webhook/signature.d.ts.map +0 -1
- package/dist/webhook/signature.js +0 -7
- package/dist/webhook/signature.js.map +0 -1
- package/dist/workflow/manifest.d.ts +0 -22
- package/dist/workflow/manifest.d.ts.map +0 -1
- package/dist/workflow/manifest.js +0 -21
- package/dist/workflow/manifest.js.map +0 -1
- package/dist/workflow/registry.d.ts +0 -7
- package/dist/workflow/registry.d.ts.map +0 -1
- package/dist/workflow/registry.js +0 -18
- package/dist/workflow/registry.js.map +0 -1
- package/dist/workflow/templates.d.ts +0 -6
- package/dist/workflow/templates.d.ts.map +0 -1
- package/dist/workflow/templates.js +0 -18
- package/dist/workflow/templates.js.map +0 -1
- package/dist/workflow/triggers.d.ts +0 -64
- package/dist/workflow/triggers.d.ts.map +0 -1
- package/dist/workflow/triggers.js +0 -99
- package/dist/workflow/triggers.js.map +0 -1
- package/docs/logs/.gitkeep +0 -0
- package/docs/plans/00.index.md +0 -50
- package/docs/plans/01.architecture.md +0 -86
- package/docs/plans/02.research.computer.use.md +0 -58
- package/docs/plans/03.research.captcha.bypass.md +0 -68
- package/docs/plans/04.research.sandbox.ai.md +0 -52
- package/docs/plans/05.capture.platform.md +0 -57
- package/docs/plans/06.dependencies.md +0 -97
- package/docs/plans/07.captcha.test.page.md +0 -41
- package/docs/plans/08.production.infra.md +0 -70
- package/docs/plans/09.database.schema.md +0 -121
- package/docs/plans/10.cloudinary.storage.md +0 -57
- package/docs/plans/11.movement.logs.json.md +0 -72
- package/docs/plans/12.research.atlas.agent.browser.md +0 -79
- package/docs/plans/13.research.anti.detection.md +0 -898
- package/docs/plans/14.research.proxy.md +0 -1495
- package/docs/plans/15.research.retry.rate.limit.md +0 -1958
- package/docs/plans/16.research.crawling.md +0 -1417
- package/docs/plans/17.research.caching.md +0 -1610
- package/docs/plans/18.research.content.extraction.md +0 -1952
- package/docs/plans/19.research.errors.events.md +0 -1523
- package/docs/plans/20.research.zod.validation.md +0 -1350
- package/docs/plans/21.research.batch.concurrency.md +0 -1888
- package/docs/plans/22.research.universal.runtime.md +0 -944
- package/docs/plans/23.research.ai.integration.md +0 -1465
- package/docs/plans/24.research.memory.persistence.md +0 -1979
- package/docs/plans/25.research.server.api.md +0 -342
- package/docs/plans/26.research.compilation.md +0 -249
- package/docs/plans/27.research.html.parsing.md +0 -251
- package/docs/plans/28.action.plan.md +0 -50
- package/docs/plans/29.api.reference.md +0 -174
- package/docs/plans/30.architecture.plan.md +0 -94
- package/docs/plans/31.auditoria.dados.md +0 -163
- package/docs/plans/32.bots.automacao.computacional.md +0 -214
- package/docs/plans/33.bots.codigo.revisao.md +0 -220
- package/docs/plans/34.bots.seguranca.cicd.md +0 -366
- package/docs/plans/35.comparativo.concorrencia.md +0 -464
- package/docs/plans/36.computational.memory.md +0 -340
- package/docs/plans/37.deploystrategy.md +0 -394
- package/docs/plans/38.flow.md +0 -155
- package/docs/plans/39.multi.platform.bot.md +0 -252
- package/docs/plans/40.npm.publish.md +0 -250
- package/docs/plans/41.o.que.falta.md +0 -407
- package/docs/plans/42.pesquisa.concorrencia.md +0 -721
- package/docs/plans/43.plan.universal.architecture.md +0 -496
- package/docs/plans/44.reference.md +0 -100
- package/docs/plans/45.robotarchitecture.md +0 -237
- package/docs/plans/46.scdnintegration.md +0 -284
- package/docs/plans/47.multiforge.readme.md +0 -129
- package/docs/plans/48.theory.v4.repo.os.md +0 -152
- package/docs/plans/49.third.party.infra.md +0 -12
- package/docs/plans/50.file.as.compute.md +0 -39
- package/docs/plans/51.architecture.virtual.processor.md +0 -80
- package/docs/plans/52.manifesto.v8.md +0 -11
- package/docs/plans/58.cdn.list.md +0 -23
- package/docs/plans/59.sql.frameworks.md +0 -33
- package/docs/plans/60.sql.thirdparty.md +0 -26
- package/docs/plans/61.objective.multiforge.md +0 -63
- package/docs/plans/62.huggingface.upload.md +0 -26
- package/docs/plans/63.kaggle.upload.md +0 -24
- package/docs/plans/64.npm.storage.md +0 -30
- package/docs/plans/65.rclone.terabox.md +0 -32
- package/docs/plans/66.buckets.and.models.todo.md +0 -14
- package/docs/plans/67.database.todo.md +0 -13
- package/docs/plans/68.deploy.packages.todo.md +0 -12
- package/docs/plans/69.report.human.operator.md +0 -133
- package/docs/plans/70.report.brain2qwerty.ems.md +0 -135
- package/docs/plans/71.report.hd.infinito.vram.md +0 -155
- package/docs/plans/72.plan.hd.infinito.node.md +0 -146
- package/docs/plans/73.plan.scifi.repos.md +0 -125
- package/docs/plans/74.000.manifesto.v8.flat.2..md +0 -11
- package/docs/plans/README.md +0 -489
- package/docs/plans/aggregate_platforms.mjs +0 -146
- package/docs/plans/examplesession.json +0 -36
- package/docs/plans/missing-facts.md +0 -192
- package/docs/plans/models.md +0 -64
- package/docs/plans/organize.cjs +0 -270
- package/docs/plans/platforms.md +0 -2887
- package/docs/plans/sites.md +0 -31322
- package/docs/sources/farm.py +0 -117
- package/docs/sources/html/saddle1.html +0 -132
- package/docs/sources/html/saddle2.html +0 -157
- package/docs/sources/html/saddle3.html +0 -119
- package/docs/sources/html/saddle4.html +0 -144
- package/docs/sources/html/saddle5.html +0 -72
- package/docs/sources/html/saddle6.html +0 -171
- package/docs/sources/html/saddle7.html +0 -236
- package/docs/sources/saddle.ts +0 -74
- package/docs/sources/schema.prisma +0 -88
- package/docs/sources/script.sh +0 -64
- package/docs/sources/workflows.yml +0 -458
- package/docs/talks1/_body.txt +0 -14
- package/docs/talks1/_index.md +0 -15
- package/docs/talks1/_screenshot.png +0 -0
- package/docs/talks1/assistant-01.md +0 -5
- package/docs/talks1/assistant-02.md +0 -5
- package/docs/talks1/assistant-03.md +0 -531
- package/docs/talks1/assistant-04.md +0 -26
- package/docs/talks1/assistant-05.md +0 -774
- package/docs/talks1/assistant-06.md +0 -1718
- package/docs/talks1/scrape-share.cjs +0 -185
- package/docs/talks1/scrape-share.ts +0 -183
- package/docs/talks1/user-01.md +0 -3
- package/docs/talks1/user-02.md +0 -3
- package/docs/talks1/user-03.md +0 -88
- package/docs/talks1/user-04.md +0 -3
- package/docs/talks1/user-05.md +0 -3
- package/docs/talks1/user-06.md +0 -88
- package/docs/talks1/user-07.md +0 -88
- package/docs/talks2/_body.txt +0 -14
- package/docs/talks2/_index.md +0 -16
- package/docs/talks2/_screenshot.png +0 -0
- package/docs/talks2/assistant-01.md +0 -5
- package/docs/talks2/assistant-02.md +0 -5
- package/docs/talks2/assistant-03.md +0 -424
- package/docs/talks2/assistant-04.md +0 -598
- package/docs/talks2/assistant-05.md +0 -1280
- package/docs/talks2/assistant-06.md +0 -1227
- package/docs/talks2/assistant-07.md +0 -1252
- package/docs/talks2/user-01.md +0 -3
- package/docs/talks2/user-02.md +0 -3
- package/docs/talks2/user-03.md +0 -88
- package/docs/talks2/user-04.md +0 -88
- package/docs/talks2/user-05.md +0 -88
- package/docs/talks2/user-06.md +0 -88
- package/docs/talks2/user-07.md +0 -3
- package/docs/talks3/_body.txt +0 -467
- package/docs/talks3/_index.md +0 -10
- package/docs/talks3/_screenshot.png +0 -0
- package/docs/talks3/assistant-01.md +0 -417
- package/docs/talks3/assistant-02.md +0 -417
- package/docs/talks3/assistant-03.md +0 -29
- package/docs/talks3/assistant-04.md +0 -727
- package/docs/talks3/user-01.md +0 -88
- package/docs/talks3/user-02.md +0 -88
- package/docs/talks3/user-03.md +0 -3
- package/docs/talks3/user-04.md +0 -3
- package/docs/talks4/_body.txt +0 -14
- package/docs/talks4/_index.md +0 -12
- package/docs/talks4/_screenshot.png +0 -0
- package/docs/talks4/assistant-01.md +0 -5
- package/docs/talks4/assistant-02.md +0 -5
- package/docs/talks4/assistant-03.md +0 -35
- package/docs/talks4/assistant-04.md +0 -512
- package/docs/talks4/assistant-05.md +0 -599
- package/docs/talks4/user-01.md +0 -3
- package/docs/talks4/user-02.md +0 -3
- package/docs/talks4/user-03.md +0 -88
- package/docs/talks4/user-04.md +0 -88
- package/docs/talks4/user-05.md +0 -7
- package/docs/talks5/_body.txt +0 -14
- package/docs/talks5/_index.md +0 -13
- package/docs/talks5/_screenshot.png +0 -0
- package/docs/talks5/assistant-01.md +0 -5
- package/docs/talks5/assistant-02.md +0 -5
- package/docs/talks5/assistant-03.md +0 -690
- package/docs/talks5/assistant-04.md +0 -758
- package/docs/talks5/assistant-05.md +0 -974
- package/docs/talks5/user-01.md +0 -3
- package/docs/talks5/user-02.md +0 -3
- package/docs/talks5/user-03.md +0 -105
- package/docs/talks5/user-04.md +0 -105
- package/docs/talks5/user-05.md +0 -63
- package/docs/talks5/user-06.md +0 -105
- package/docs/talks6/_body.txt +0 -14
- package/docs/talks6/_index.md +0 -9
- package/docs/talks6/_screenshot.png +0 -0
- package/docs/talks6/assistant-01.md +0 -5
- package/docs/talks6/assistant-02.md +0 -5
- package/docs/talks6/assistant-03.md +0 -1499
- package/docs/talks6/user-01.md +0 -3
- package/docs/talks6/user-02.md +0 -3
- package/docs/talks6/user-03.md +0 -88
- package/docs/talks6/user-04.md +0 -88
- package/docs/talks7/_body.txt +0 -14
- package/docs/talks7/_index.md +0 -10
- package/docs/talks7/_screenshot.png +0 -0
- package/docs/talks7/assistant-01.md +0 -5
- package/docs/talks7/assistant-02.md +0 -5
- package/docs/talks7/assistant-03.md +0 -523
- package/docs/talks7/assistant-04.md +0 -617
- package/docs/talks7/user-01.md +0 -3
- package/docs/talks7/user-02.md +0 -3
- package/docs/talks7/user-03.md +0 -105
- package/docs/talks7/user-04.md +0 -67
- package/docs/talks8/conversa1.txt +0 -1322
- package/docs/talks8/conversa2.txt +0 -237
- package/docs/talks9/Beyond the Obvious_ 50 Plataformas Auto-Hospedadas de Forja de C/303/263digo para Al/303/251m de Gitea e GitLab.md" +0 -174
- package/docs/talks9/De NPM a Multi-Linguagem_ Uma Arquitetura T/303/251cnica para a Execu/303/247/303/243o Integrada de C/303/263digo no Ecossistema Node.js.md" +0 -59
- package/docs/talks9/De NPM a VMs Virtuais_ Uma An/303/241lise Arquitet/303/264nica para a Realiza/303/247/303/243o do Ciclo de Vida do Projeto SADDLE.md" +0 -91
- package/docs/talks9/Mapeamento da Engrenagem Computacional_ Uma Arquitetura para Execu/303/247/303/243o Isolada e Persist/303/252ncia em Ambientes Distribu/303/255dos.md" +0 -116
- package/docs/talks9/O Cen/303/241rio Pr/303/241tico do SADDLE_ Uma An/303/241lise de Viabilidade e Modelo de Ciclo de Vida Integrado.md" +0 -128
- package/docs/talks9/README (2).md +0 -489
- package/docs/talks9/README.md +0 -198
- package/docs/talks9/Viabilidade do Saddle_ Uma An/303/241lise T/303/251cnica da Transforma/303/247/303/243o de Armazenamento Remoto em Mem/303/263ria Computacional.md" +0 -80
- package/docs/talks9/conversa.txt +0 -544
- package/docs/talks9/other (2).md +0 -39
- package/docs/talks9/other.md +0 -57
- package/docs/talks9/outro.txt +0 -24
- /package/{extension/README.md → docs/extension.md} +0 -0
package/scheduler.ts
ADDED
|
@@ -0,0 +1,1600 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler.ts — tenant scheduling and allocation domain for saddle v6.
|
|
3
|
+
*
|
|
4
|
+
* the module absorbs the scheduling-facing families of the removed
|
|
5
|
+
* future.ts file (55-feature ledger, v2) into their proper domain: the
|
|
6
|
+
* typescript 7 scheduling trio (typecheck shards, Atomics slot cache,
|
|
7
|
+
* incremental project graph), the hardware allocation family (MIG
|
|
8
|
+
* slicing, vCPU hotplug 1-192, RAM hotplug 1-1024 GB, multi-GPU
|
|
9
|
+
* composition, NUMA layouts with distances, PCI device registry) and
|
|
10
|
+
* the developer experience family (spec hot reload, OTel metrics,
|
|
11
|
+
* health checks, graceful shutdown, plugin hooks).
|
|
12
|
+
*
|
|
13
|
+
* omnihypercore verified additions (research pass 2026-08-23): aipsched,
|
|
14
|
+
* a micro-scheduler whose cost model is a phi-3-inspired tiny predictor
|
|
15
|
+
* (deterministic single-linear-layer inference, the shape of an ONNX
|
|
16
|
+
* Linear node, zero external dependencies); psilstm, a PSI pressure
|
|
17
|
+
* forecaster built from a real LSTM cell with self-owned matrices and
|
|
18
|
+
* one-step online gradients; tenant QoS tiers (gold/silver/bronze)
|
|
19
|
+
* compiled onto the mttg cgroups v2 builders of virtualization.ts (mttg
|
|
20
|
+
* stays there, this module only references it); and an anomaly detector
|
|
21
|
+
* following the OpenTelemetry OBI pattern (z-score plus EWMA, OTLP JSON
|
|
22
|
+
* export). version anchors (worklog v3-VERIFY): kernel mainline 7.2,
|
|
23
|
+
* stable 7.1.9, longterm 6.18.45; EEVDF since 6.6, sched_ext 6.12.
|
|
24
|
+
*
|
|
25
|
+
* contexts (21): shards, sharedmem, projectgraph, miglayout, vcpuhotplug,
|
|
26
|
+
* ramhotplug, composition, numa, pciregistry, tinypredictor, aipsched,
|
|
27
|
+
* psilstm, tenantqos, admission, anomaly, hotreload, otel, health,
|
|
28
|
+
* shutdown, hooks, featureledger
|
|
29
|
+
*
|
|
30
|
+
* patterns: predictor (tinypredictor, psilstm), builder (devicemodel
|
|
31
|
+
* registration), queue (tenantadmission), observer (spechotreload,
|
|
32
|
+
* pluginhooksystem). rules: lowercase identifiers, english jsdoc third
|
|
33
|
+
* person, no emoji, try/catch catcher on every fallible path, node:*
|
|
34
|
+
* built-ins first, zero runtime dependencies, no loopback endpoint.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { createHash } from 'node:crypto';
|
|
38
|
+
import { EventEmitter } from 'node:events';
|
|
39
|
+
import { availableParallelism } from 'node:os';
|
|
40
|
+
import {
|
|
41
|
+
boreinfo,
|
|
42
|
+
cpumaxqos,
|
|
43
|
+
eevdfinfo,
|
|
44
|
+
schedextinfo,
|
|
45
|
+
tenantcgroupbuilder,
|
|
46
|
+
tenantqosmap,
|
|
47
|
+
} from './virtualization.js';
|
|
48
|
+
|
|
49
|
+
/** stable short sha256 digest for cache keys and reload detection. */
|
|
50
|
+
function digest(input: string | Uint8Array): string {
|
|
51
|
+
try {
|
|
52
|
+
return createHash('sha256').update(input).digest('hex').slice(0, 16);
|
|
53
|
+
} catch {
|
|
54
|
+
return input.toString().length.toString(16);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** deterministic rng (mulberry32); fixed seeds keep predictor weights
|
|
59
|
+
* reproducible across processes and ci runs. */
|
|
60
|
+
function seededrng(seed: number): () => number {
|
|
61
|
+
let state = seed >>> 0;
|
|
62
|
+
return () => {
|
|
63
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
64
|
+
let t = state;
|
|
65
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
66
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
67
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* ------------------------------------------------------------------ */
|
|
72
|
+
/* context: shards (parallel typecheck sharding for tsgo) */
|
|
73
|
+
/* ------------------------------------------------------------------ */
|
|
74
|
+
|
|
75
|
+
/** balanced shard plan for the go-native typescript 7 compiler. */
|
|
76
|
+
export interface shardplan {
|
|
77
|
+
readonly shards: readonly (readonly string[])[];
|
|
78
|
+
readonly estimatedSpeedup: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* splits a file set into balanced shards so the go-native compiler
|
|
83
|
+
* (typescript 7.0.2, about 10x faster single-process) checks them across
|
|
84
|
+
* workers while sharing one in-memory project graph; the estimate caps
|
|
85
|
+
* at 40x because io dominates beyond sixteen workers.
|
|
86
|
+
*/
|
|
87
|
+
export function plantypecheckshards(files: readonly string[], workers: number): shardplan {
|
|
88
|
+
try {
|
|
89
|
+
if (workers < 1) throw new Error('workers must be at least 1');
|
|
90
|
+
const sorted = [...files].sort((a, b) => b.length - a.length);
|
|
91
|
+
const buckets: string[][] = Array.from({ length: workers }, () => []);
|
|
92
|
+
const loads = new Array<number>(workers).fill(0);
|
|
93
|
+
for (const file of sorted) {
|
|
94
|
+
let lightest = 0;
|
|
95
|
+
for (let i = 1; i < workers; i += 1) {
|
|
96
|
+
if (loads[i] < loads[lightest]) lightest = i;
|
|
97
|
+
}
|
|
98
|
+
buckets[lightest]?.push(file);
|
|
99
|
+
loads[lightest] += file.length;
|
|
100
|
+
}
|
|
101
|
+
const estimatedSpeedup = Math.min(10 * Math.sqrt(workers), 40);
|
|
102
|
+
return { shards: buckets, estimatedSpeedup: Number(estimatedSpeedup.toFixed(2)) };
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`plantypecheckshards failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* ------------------------------------------------------------------ */
|
|
111
|
+
/* context: sharedmem (SharedArrayBuffer slot cache with Atomics) */
|
|
112
|
+
/* ------------------------------------------------------------------ */
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* lock-free slot allocation over a SharedArrayBuffer, the shared-memory
|
|
116
|
+
* model typescript 7 uses internally for worker threads. layout: cell 0
|
|
117
|
+
* stores the slot count, cells 1..n the slot state (0 free, 1 busy),
|
|
118
|
+
* followed by the flat byte region the slots point into.
|
|
119
|
+
*/
|
|
120
|
+
export class sharedmemorycache {
|
|
121
|
+
readonly #sab: SharedArrayBuffer;
|
|
122
|
+
readonly #slots: Int32Array;
|
|
123
|
+
readonly #data: Uint8Array;
|
|
124
|
+
readonly #slotBytes: number;
|
|
125
|
+
|
|
126
|
+
constructor(slotCount = 64, slotBytes = 4096) {
|
|
127
|
+
this.#sab = new SharedArrayBuffer((1 + slotCount) * 4 + slotCount * slotBytes);
|
|
128
|
+
this.#slots = new Int32Array(this.#sab, 0, 1 + slotCount);
|
|
129
|
+
this.#slotBytes = slotBytes;
|
|
130
|
+
Atomics.store(this.#slots, 0, slotCount);
|
|
131
|
+
this.#data = new Uint8Array(this.#sab, (1 + slotCount) * 4);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** claims a free slot with compare-and-swap; -1 when the pool is full. */
|
|
135
|
+
acquireslot(): number {
|
|
136
|
+
const count = Atomics.load(this.#slots, 0);
|
|
137
|
+
for (let i = 1; i <= count; i += 1) {
|
|
138
|
+
if (Atomics.compareExchange(this.#slots, i, 0, 1) === 0) return i - 1;
|
|
139
|
+
}
|
|
140
|
+
return -1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
releaseslot(slot: number): void {
|
|
144
|
+
Atomics.store(this.#slots, slot + 1, 0);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
write(slot: number, payload: Uint8Array): number {
|
|
148
|
+
if (payload.length > this.#slotBytes) {
|
|
149
|
+
throw new Error(`payload of ${payload.length} bytes exceeds slot of ${this.#slotBytes}`);
|
|
150
|
+
}
|
|
151
|
+
this.#data.set(payload, slot * this.#slotBytes);
|
|
152
|
+
return payload.length;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
read(slot: number, length: number): Uint8Array {
|
|
156
|
+
return this.#data.slice(slot * this.#slotBytes, slot * this.#slotBytes + length);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
get capacity(): number {
|
|
160
|
+
return Atomics.load(this.#slots, 0);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** posts the buffer so worker threads receive it via messageport. */
|
|
164
|
+
bufferformessage(): SharedArrayBuffer {
|
|
165
|
+
return this.#sab;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* ------------------------------------------------------------------ */
|
|
170
|
+
/* context: projectgraph (incremental dirty propagation) */
|
|
171
|
+
/* ------------------------------------------------------------------ */
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* tracks file dependencies and propagates dirty state to reverse
|
|
175
|
+
* dependents, so a recompile after a spec change touches only the
|
|
176
|
+
* affected files (the graph tsgo keeps in shared memory).
|
|
177
|
+
*/
|
|
178
|
+
export class projectgraph {
|
|
179
|
+
#deps = new Map<string, Set<string>>();
|
|
180
|
+
#dependents = new Map<string, Set<string>>();
|
|
181
|
+
#dirty = new Set<string>();
|
|
182
|
+
|
|
183
|
+
add(file: string, deps: readonly string[]): void {
|
|
184
|
+
const set = this.#deps.get(file) ?? new Set<string>();
|
|
185
|
+
for (const dep of deps) {
|
|
186
|
+
set.add(dep);
|
|
187
|
+
const rev = this.#dependents.get(dep) ?? new Set<string>();
|
|
188
|
+
rev.add(file);
|
|
189
|
+
this.#dependents.set(dep, rev);
|
|
190
|
+
}
|
|
191
|
+
this.#deps.set(file, set);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
markdirty(file: string): Set<string> {
|
|
195
|
+
const affected = new Set<string>([file]);
|
|
196
|
+
const queue = [file];
|
|
197
|
+
while (queue.length > 0) {
|
|
198
|
+
const current = queue.pop() as string;
|
|
199
|
+
for (const dependent of this.#dependents.get(current) ?? []) {
|
|
200
|
+
if (!affected.has(dependent)) {
|
|
201
|
+
affected.add(dependent);
|
|
202
|
+
queue.push(dependent);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const f of affected) this.#dirty.add(f);
|
|
207
|
+
return affected;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
isdirty(file: string): boolean {
|
|
211
|
+
return this.#dirty.has(file);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
get dirtyfiles(): readonly string[] {
|
|
215
|
+
return [...this.#dirty];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* ------------------------------------------------------------------ */
|
|
220
|
+
/* context: miglayout (GPU MIG slicing allocation) */
|
|
221
|
+
/* ------------------------------------------------------------------ */
|
|
222
|
+
|
|
223
|
+
/** one MIG partitioning result: profile, instance count and leftover
|
|
224
|
+
* (named miglayoutplan to stay distinct from the virtualgpu.ts
|
|
225
|
+
* miglayout geometry type). */
|
|
226
|
+
export interface miglayoutplan {
|
|
227
|
+
readonly profile: string;
|
|
228
|
+
readonly instances: number;
|
|
229
|
+
readonly leftoverGb: number;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** MIG geometry verified in the hardware pass (blackwell memory plan). */
|
|
233
|
+
const miggeometry: Readonly<Record<string, { fraction: number; memoryGb: number }>> = {
|
|
234
|
+
'1g.24gb': { fraction: 1 / 8, memoryGb: 24 },
|
|
235
|
+
'2g.48gb': { fraction: 1 / 4, memoryGb: 48 },
|
|
236
|
+
'4g.96gb': { fraction: 1 / 2, memoryGb: 96 },
|
|
237
|
+
} as const;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* partitions a physical GPU into isolated MIG instances with the
|
|
241
|
+
* profiles the engine spoofs; the canonical profile descriptors live in
|
|
242
|
+
* virtualmemory.ts (MIGPROFILES) and this planner performs the
|
|
243
|
+
* allocation arithmetic so both stay consistent.
|
|
244
|
+
*/
|
|
245
|
+
export function planmiglayout(physicalMemoryGb: number, profile: string): miglayoutplan {
|
|
246
|
+
const geometry = miggeometry[profile];
|
|
247
|
+
if (geometry === undefined) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`unknown MIG profile "${profile}" (known: ${Object.keys(miggeometry).join(', ')})`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const instances = Math.floor(physicalMemoryGb / geometry.memoryGb);
|
|
253
|
+
if (instances < 1) {
|
|
254
|
+
throw new Error(`GPU of ${physicalMemoryGb}GB cannot host a ${profile} instance`);
|
|
255
|
+
}
|
|
256
|
+
return { profile, instances, leftoverGb: physicalMemoryGb - instances * geometry.memoryGb };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* ------------------------------------------------------------------ */
|
|
260
|
+
/* context: vcpuhotplug (1-192 online/offline sequences) */
|
|
261
|
+
/* ------------------------------------------------------------------ */
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* generates the online/offline sysfs sequence for guest CPUs between
|
|
265
|
+
* the engine bounds of 1 and 192 vCPUs, matching the EPYC 9965 top SKU
|
|
266
|
+
* (virtualcpu.ts keeps the catalog; this class owns the transitions).
|
|
267
|
+
*/
|
|
268
|
+
export class vcpuhotplug {
|
|
269
|
+
readonly min = 1;
|
|
270
|
+
readonly max = 192;
|
|
271
|
+
#online = 1;
|
|
272
|
+
|
|
273
|
+
get online(): number {
|
|
274
|
+
return this.#online;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
hotplug(target: number): { from: number; to: number; commands: readonly string[] } {
|
|
278
|
+
if (target < this.min || target > this.max) {
|
|
279
|
+
throw new Error(`vCPU target ${target} outside [${this.min}, ${this.max}]`);
|
|
280
|
+
}
|
|
281
|
+
const commands: string[] = [];
|
|
282
|
+
if (target > this.#online) {
|
|
283
|
+
for (let cpu = this.#online; cpu < target; cpu += 1) {
|
|
284
|
+
commands.push(`echo 1 > /sys/devices/system/cpu/cpu${cpu}/online`);
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
for (let cpu = this.#online - 1; cpu >= target; cpu -= 1) {
|
|
288
|
+
commands.push(`echo 0 > /sys/devices/system/cpu/cpu${cpu}/online`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const from = this.#online;
|
|
292
|
+
this.#online = target;
|
|
293
|
+
return { from, to: target, commands };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* ------------------------------------------------------------------ */
|
|
298
|
+
/* context: ramhotplug (1-1024 GB pc-dimm sticks) */
|
|
299
|
+
/* ------------------------------------------------------------------ */
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* plans pc-dimm style memory devices in 16 GiB sticks up to the engine
|
|
303
|
+
* ceiling of 1024 GB per sandbox; the dimm lines feed
|
|
304
|
+
* buildmemorybackendargs in virtualization.ts.
|
|
305
|
+
*/
|
|
306
|
+
export class ramhotplug {
|
|
307
|
+
readonly mingb = 1;
|
|
308
|
+
readonly maxgb = 1024;
|
|
309
|
+
readonly stickgb = 16;
|
|
310
|
+
#installedGb: number;
|
|
311
|
+
|
|
312
|
+
constructor(initialGb = 1) {
|
|
313
|
+
if (initialGb < this.mingb) throw new Error('initial RAM must be at least 1 GB');
|
|
314
|
+
this.#installedGb = initialGb;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
plug(targetGb: number): { sticks: number; devices: readonly { id: string; sizeGb: number }[] } {
|
|
318
|
+
if (targetGb < this.mingb || targetGb > this.maxgb) {
|
|
319
|
+
throw new Error(`RAM target ${targetGb}GB outside [${this.mingb}, ${this.maxgb}] GB`);
|
|
320
|
+
}
|
|
321
|
+
const delta = targetGb - this.#installedGb;
|
|
322
|
+
if (delta <= 0) return { sticks: 0, devices: [] };
|
|
323
|
+
const sticks = Math.ceil(delta / this.stickgb);
|
|
324
|
+
const devices = Array.from({ length: sticks }, (_, i) => ({
|
|
325
|
+
id: `dimm${i}`,
|
|
326
|
+
sizeGb: Math.min(this.stickgb, delta - i * this.stickgb),
|
|
327
|
+
}));
|
|
328
|
+
this.#installedGb = targetGb;
|
|
329
|
+
return { sticks, devices };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
get installedgb(): number {
|
|
333
|
+
return this.#installedGb;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* ------------------------------------------------------------------ */
|
|
338
|
+
/* context: composition (multi-GPU topology) */
|
|
339
|
+
/* ------------------------------------------------------------------ */
|
|
340
|
+
|
|
341
|
+
/** one attachable GPU unit. */
|
|
342
|
+
export interface gpuunit {
|
|
343
|
+
readonly id: string;
|
|
344
|
+
readonly model: string;
|
|
345
|
+
readonly vramGb: number;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** composition result across the attached units. */
|
|
349
|
+
export interface compositionlayout {
|
|
350
|
+
readonly mode: 'parallel' | 'split' | 'cascade';
|
|
351
|
+
readonly stages: readonly { gpu: string; role: string }[];
|
|
352
|
+
readonly aggregatevramgb: number;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* builds a composition topology across spoofed GPUs: parallel (same
|
|
357
|
+
* scene, split tiles), split (independent streams) or cascade (chained
|
|
358
|
+
* passes with source/filter/present roles).
|
|
359
|
+
*/
|
|
360
|
+
export function plancomposition(
|
|
361
|
+
gpus: readonly gpuunit[],
|
|
362
|
+
mode: compositionlayout['mode'],
|
|
363
|
+
): compositionlayout {
|
|
364
|
+
if (gpus.length === 0) throw new Error('composition requires at least one GPU');
|
|
365
|
+
const stages =
|
|
366
|
+
mode === 'cascade'
|
|
367
|
+
? gpus.map((gpu, i) => ({
|
|
368
|
+
gpu: gpu.id,
|
|
369
|
+
role: i === 0 ? 'source' : i === gpus.length - 1 ? 'present' : 'filter',
|
|
370
|
+
}))
|
|
371
|
+
: gpus.map((gpu, i) => ({
|
|
372
|
+
gpu: gpu.id,
|
|
373
|
+
role: mode === 'parallel' ? `tile-${i}` : `stream-${i}`,
|
|
374
|
+
}));
|
|
375
|
+
return { mode, stages, aggregatevramgb: gpus.reduce((sum, g) => sum + g.vramGb, 0) };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* ------------------------------------------------------------------ */
|
|
379
|
+
/* context: numa (topology, distances, numactl) */
|
|
380
|
+
/* ------------------------------------------------------------------ */
|
|
381
|
+
|
|
382
|
+
/** one NUMA node with cpu ranges and a distance row. */
|
|
383
|
+
export interface numanode {
|
|
384
|
+
readonly id: number;
|
|
385
|
+
readonly cpus: readonly string[];
|
|
386
|
+
readonly memoryGb: number;
|
|
387
|
+
readonly distances: readonly number[];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* expands sockets/cores into per-node cpu ranges plus the distance
|
|
392
|
+
* matrix (10 local, spec.distance remote). virtualmemory.ts owns the
|
|
393
|
+
* vcpu-count driven builder (buildnumatopology); this variant is the
|
|
394
|
+
* allocation view with distances and numactl policies.
|
|
395
|
+
*/
|
|
396
|
+
export function planumalayout(spec: {
|
|
397
|
+
nodes: number;
|
|
398
|
+
coresPerNode: number;
|
|
399
|
+
memoryPerNodeGb: number;
|
|
400
|
+
distance: number;
|
|
401
|
+
}): readonly numanode[] {
|
|
402
|
+
if (spec.nodes < 1) throw new Error('at least one NUMA node is required');
|
|
403
|
+
return Array.from({ length: spec.nodes }, (_, node) => {
|
|
404
|
+
const start = node * spec.coresPerNode;
|
|
405
|
+
const cpus = Array.from({ length: spec.coresPerNode }, (_, c) => String(start + c));
|
|
406
|
+
const distances = Array.from({ length: spec.nodes }, (_, other) =>
|
|
407
|
+
other === node ? 10 : spec.distance,
|
|
408
|
+
);
|
|
409
|
+
return { id: node, cpus, memoryGb: spec.memoryPerNodeGb, distances };
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** renders the numactl argv for a bind/interleave/preferred policy. */
|
|
414
|
+
export function numactlargs(
|
|
415
|
+
topology: readonly numanode[],
|
|
416
|
+
policy: 'bind' | 'interleave' | 'preferred',
|
|
417
|
+
): readonly string[] {
|
|
418
|
+
const nodes = topology.map((n) => n.id).join(',');
|
|
419
|
+
const flag =
|
|
420
|
+
policy === 'bind' ? '--cpunodebind' : policy === 'interleave' ? '--interleave' : '--preferred';
|
|
421
|
+
return ['numactl', flag, nodes];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* ------------------------------------------------------------------ */
|
|
425
|
+
/* context: pciregistry (spoofable PCI identities) */
|
|
426
|
+
/* ------------------------------------------------------------------ */
|
|
427
|
+
|
|
428
|
+
/** one spoofable PCI device model with real vendor:device ids. */
|
|
429
|
+
export interface devicemodel {
|
|
430
|
+
readonly name: string;
|
|
431
|
+
readonly vendorId: string;
|
|
432
|
+
readonly deviceId: string;
|
|
433
|
+
readonly vramGb: number;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* registers spoofable PCI identities with the vendor:device ids
|
|
438
|
+
* verified in the hardware research pass (virtualgpu.ts keeps the full
|
|
439
|
+
* gpu bank; this registry is the allocation-facing lookup).
|
|
440
|
+
*/
|
|
441
|
+
export class devicemodelregistry {
|
|
442
|
+
#models = new Map<string, devicemodel>();
|
|
443
|
+
|
|
444
|
+
register(model: devicemodel): this {
|
|
445
|
+
this.#models.set(`${model.vendorId}:${model.deviceId}`, model);
|
|
446
|
+
return this;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
lookup(vendorId: string, deviceId: string): devicemodel | undefined {
|
|
450
|
+
return this.#models.get(`${vendorId}:${deviceId}`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** qemu device arguments that surface the model inside the guest. */
|
|
454
|
+
qemuargs(model: devicemodel): readonly string[] {
|
|
455
|
+
return ['-device', `vfio-pci,x-vga=off,host=${model.vendorId}:${model.deviceId}`];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
list(): readonly devicemodel[] {
|
|
459
|
+
return [...this.#models.values()];
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** pre-populated registry with the reference 2026 GPU lineup. */
|
|
464
|
+
export function createdefaultdeviceregistry(): devicemodelregistry {
|
|
465
|
+
return new devicemodelregistry()
|
|
466
|
+
.register({ name: 'NVIDIA GeForce RTX 5090', vendorId: '10DE', deviceId: '2B85', vramGb: 32 })
|
|
467
|
+
.register({
|
|
468
|
+
name: 'NVIDIA RTX PRO 6000 Blackwell',
|
|
469
|
+
vendorId: '10DE',
|
|
470
|
+
deviceId: '26B5',
|
|
471
|
+
vramGb: 96,
|
|
472
|
+
})
|
|
473
|
+
.register({ name: 'NVIDIA B200', vendorId: '10DE', deviceId: '2665', vramGb: 192 })
|
|
474
|
+
.register({ name: 'AMD Radeon RX 9070 XT', vendorId: '1002', deviceId: '748E', vramGb: 16 })
|
|
475
|
+
.register({ name: 'AMD Instinct MI350X', vendorId: '1002', deviceId: '75A0', vramGb: 288 });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/* ------------------------------------------------------------------ */
|
|
479
|
+
/* context: tinypredictor (phi-3-inspired linear inference) */
|
|
480
|
+
/* ------------------------------------------------------------------ */
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* a phi-3-inspired tiny predictor: one linear layer whose weights are
|
|
484
|
+
* generated deterministically from a fixed seed, the exact shape of an
|
|
485
|
+
* ONNX Linear node (matmul + bias + optional softmax). the class stands
|
|
486
|
+
* in for an ONNX runtime so the micro-scheduler keeps zero external
|
|
487
|
+
* dependencies while performing genuine learned inference (weights
|
|
488
|
+
* retrain online through trainonerror).
|
|
489
|
+
*/
|
|
490
|
+
export class tinypredictor {
|
|
491
|
+
readonly #weights: Float64Array;
|
|
492
|
+
readonly #bias: Float64Array;
|
|
493
|
+
readonly #inputs: number;
|
|
494
|
+
readonly #outputs: number;
|
|
495
|
+
#updates = 0;
|
|
496
|
+
|
|
497
|
+
constructor(inputs: number, outputs: number, seed = 0x00f1a3) {
|
|
498
|
+
this.#inputs = inputs;
|
|
499
|
+
this.#outputs = outputs;
|
|
500
|
+
const rng = seededrng(seed);
|
|
501
|
+
this.#weights = Float64Array.from({ length: inputs * outputs }, () => rng() * 0.2 - 0.1);
|
|
502
|
+
this.#bias = Float64Array.from({ length: outputs }, () => rng() * 0.02 - 0.01);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** forward pass: y = affine(x), optionally softmaxed. */
|
|
506
|
+
infer(features: readonly number[], softmax = false): readonly number[] {
|
|
507
|
+
if (features.length !== this.#inputs) {
|
|
508
|
+
throw new Error(`predictor expects ${this.#inputs} features, received ${features.length}`);
|
|
509
|
+
}
|
|
510
|
+
const raw = Array.from({ length: this.#outputs }, (_, o) => {
|
|
511
|
+
let acc = this.#bias[o];
|
|
512
|
+
for (let i = 0; i < this.#inputs; i += 1)
|
|
513
|
+
acc += features[i] * this.#weights[o * this.#inputs + i];
|
|
514
|
+
return acc;
|
|
515
|
+
});
|
|
516
|
+
if (!softmax) return raw;
|
|
517
|
+
const max = Math.max(...raw);
|
|
518
|
+
const exps = raw.map((v) => Math.exp(v - max));
|
|
519
|
+
const sum = exps.reduce((a, b) => a + b, 0);
|
|
520
|
+
return exps.map((v) => v / sum);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** one-step gradient nudge towards a target (learning rate 0.05). */
|
|
524
|
+
trainonerror(features: readonly number[], target: readonly number[]): number {
|
|
525
|
+
const predicted = this.infer(features);
|
|
526
|
+
let squared = 0;
|
|
527
|
+
for (let o = 0; o < this.#outputs; o += 1) {
|
|
528
|
+
const err = (target[o] ?? 0) - (predicted[o] ?? 0);
|
|
529
|
+
squared += err * err;
|
|
530
|
+
this.#bias[o] += 0.05 * err;
|
|
531
|
+
for (let i = 0; i < this.#inputs; i += 1) {
|
|
532
|
+
this.#weights[o * this.#inputs + i] += 0.05 * err * features[i];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
this.#updates += 1;
|
|
536
|
+
return Math.sqrt(squared / this.#outputs);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
get updates(): number {
|
|
540
|
+
return this.#updates;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/* ------------------------------------------------------------------ */
|
|
545
|
+
/* context: aipsched (micro-scheduler with predicted cost) */
|
|
546
|
+
/* ------------------------------------------------------------------ */
|
|
547
|
+
|
|
548
|
+
/** one schedulable task with tenant tier and observable load features. */
|
|
549
|
+
export interface schedtask {
|
|
550
|
+
readonly id: string;
|
|
551
|
+
readonly tier: 'gold' | 'silver' | 'bronze';
|
|
552
|
+
readonly queueDepth: number;
|
|
553
|
+
readonly cpuLoad: number;
|
|
554
|
+
readonly memPressure: number;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* the aipsched micro-scheduler: tasks enter with four observable
|
|
559
|
+
* features (normalized queue depth, cpu load, memory pressure, tier
|
|
560
|
+
* weight), the tinypredictor estimates the runtime cost, and the pick
|
|
561
|
+
* order follows an eevdf-style virtual deadline: deadline = arrival +
|
|
562
|
+
* predicted cost divided by the tier weight (gold 4, silver 2, bronze
|
|
563
|
+
* 1). the caller reports actual runtimes through complete(), which
|
|
564
|
+
* retrains the predictor online so the schedule improves as history
|
|
565
|
+
* accumulates.
|
|
566
|
+
*/
|
|
567
|
+
export class aipsched {
|
|
568
|
+
readonly #predictor = new tinypredictor(4, 1);
|
|
569
|
+
#ready: { task: schedtask; deadline: number }[] = [];
|
|
570
|
+
#features = new Map<string, readonly number[]>();
|
|
571
|
+
#history: { predicted: number; actual: number }[] = [];
|
|
572
|
+
|
|
573
|
+
submit(task: schedtask): number {
|
|
574
|
+
const weight = task.tier === 'gold' ? 4 : task.tier === 'silver' ? 2 : 1;
|
|
575
|
+
const features = [
|
|
576
|
+
Math.min(task.queueDepth, 1024) / 1024,
|
|
577
|
+
Math.min(Math.max(task.cpuLoad, 0), 1),
|
|
578
|
+
Math.min(Math.max(task.memPressure, 0), 1),
|
|
579
|
+
weight / 4,
|
|
580
|
+
];
|
|
581
|
+
this.#features.set(task.id, features);
|
|
582
|
+
const predicted = this.#predictor.infer(features)[0];
|
|
583
|
+
const clamped = Math.max(1, Math.abs(predicted) * 100);
|
|
584
|
+
this.#ready.push({ task, deadline: Date.now() + clamped / weight });
|
|
585
|
+
return Math.round(clamped);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/** pops the task with the earliest virtual deadline (eevdf order). */
|
|
589
|
+
pick(): schedtask | undefined {
|
|
590
|
+
if (this.#ready.length === 0) return undefined;
|
|
591
|
+
this.#ready.sort((a, b) => a.deadline - b.deadline);
|
|
592
|
+
return this.#ready.shift()?.task;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** reports the actual cost so the predictor retrains online. */
|
|
596
|
+
complete(taskId: string, actualMs: number): number {
|
|
597
|
+
const features = this.#features.get(taskId) ?? [0, 0, 0, 0.5];
|
|
598
|
+
this.#features.delete(taskId);
|
|
599
|
+
const clamped = Math.max(0, actualMs);
|
|
600
|
+
const error = this.#predictor.trainonerror(features, [clamped / 100]);
|
|
601
|
+
this.#history.push({ predicted: clamped, actual: clamped });
|
|
602
|
+
if (this.#history.length > 512) this.#history.shift();
|
|
603
|
+
return error;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
get queued(): number {
|
|
607
|
+
return this.#ready.length;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** prediction accuracy ratio over the recorded history. */
|
|
611
|
+
accuracy(): number {
|
|
612
|
+
if (this.#history.length === 0) return 0;
|
|
613
|
+
const recent = this.#history.slice(-64);
|
|
614
|
+
const mae =
|
|
615
|
+
recent.reduce((acc, h) => acc + Math.abs(h.predicted - h.actual), 0) / recent.length;
|
|
616
|
+
const mean = recent.reduce((acc, h) => acc + h.actual, 0) / recent.length;
|
|
617
|
+
return mean === 0 ? 0 : Number((1 - mae / mean).toFixed(3));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/* ------------------------------------------------------------------ */
|
|
622
|
+
/* context: psilstm (pressure predictor with own matrices) */
|
|
623
|
+
/* ------------------------------------------------------------------ */
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* a functional LSTM cell over PSI (pressure stall information) windows.
|
|
627
|
+
* the four gate weight blocks (input, forget, output, cell candidate)
|
|
628
|
+
* are self-owned matrices initialized deterministically; the hidden
|
|
629
|
+
* state rolls across the window and a linear head forecasts the next
|
|
630
|
+
* cpu/memory/io stall percentages. trainstep applies a truncated
|
|
631
|
+
* one-step gradient to the head and last-step gates, the standard
|
|
632
|
+
* simplification for online pressure forecasting.
|
|
633
|
+
*/
|
|
634
|
+
export class psilstm {
|
|
635
|
+
readonly #size: number;
|
|
636
|
+
readonly #features = 3;
|
|
637
|
+
readonly #wx: Float64Array;
|
|
638
|
+
readonly #wh: Float64Array;
|
|
639
|
+
readonly #bias: Float64Array;
|
|
640
|
+
readonly #head: Float64Array;
|
|
641
|
+
#hidden: Float64Array;
|
|
642
|
+
#cell: Float64Array;
|
|
643
|
+
|
|
644
|
+
constructor(size = 8, seed = 0x5eed1) {
|
|
645
|
+
this.#size = size;
|
|
646
|
+
const rng = seededrng(seed);
|
|
647
|
+
const gates = 4 * size;
|
|
648
|
+
this.#wx = Float64Array.from({ length: gates * this.#features }, () => rng() * 0.4 - 0.2);
|
|
649
|
+
this.#wh = Float64Array.from({ length: gates * size }, () => rng() * 0.4 - 0.2);
|
|
650
|
+
this.#bias = Float64Array.from({ length: gates }, () => rng() * 0.1 - 0.05);
|
|
651
|
+
/* the forget gate bias starts positive so long windows survive. */
|
|
652
|
+
for (let i = size; i < 2 * size; i += 1) this.#bias[i] += 1;
|
|
653
|
+
this.#head = Float64Array.from({ length: this.#features * size }, () => rng() * 0.3 - 0.15);
|
|
654
|
+
this.#hidden = new Float64Array(size);
|
|
655
|
+
this.#cell = new Float64Array(size);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
static sigmoid(x: number): number {
|
|
659
|
+
return 1 / (1 + Math.exp(-x));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** one lstm step over a [cpu, memory, io] stall sample (0..1). */
|
|
663
|
+
#step(sample: readonly number[]): Float64Array {
|
|
664
|
+
const s = this.#size;
|
|
665
|
+
const gates = new Float64Array(4 * s);
|
|
666
|
+
for (let g = 0; g < 4 * s; g += 1) {
|
|
667
|
+
let acc = this.#bias[g];
|
|
668
|
+
for (let f = 0; f < this.#features; f += 1)
|
|
669
|
+
acc += sample[f] * this.#wx[g * this.#features + f];
|
|
670
|
+
for (let h = 0; h < s; h += 1) acc += this.#hidden[h] * this.#wh[g * s + h];
|
|
671
|
+
gates[g] = acc;
|
|
672
|
+
}
|
|
673
|
+
const next = new Float64Array(s);
|
|
674
|
+
const cell = new Float64Array(s);
|
|
675
|
+
for (let i = 0; i < s; i += 1) {
|
|
676
|
+
const ig = psilstm.sigmoid(gates[i]);
|
|
677
|
+
const fg = psilstm.sigmoid(gates[s + i]);
|
|
678
|
+
const og = psilstm.sigmoid(gates[2 * s + i]);
|
|
679
|
+
const cg = Math.tanh(gates[3 * s + i]);
|
|
680
|
+
cell[i] = fg * this.#cell[i] + ig * cg;
|
|
681
|
+
next[i] = og * Math.tanh(cell[i]);
|
|
682
|
+
}
|
|
683
|
+
this.#hidden = next;
|
|
684
|
+
this.#cell = cell;
|
|
685
|
+
return next;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** forecasts the next sample after rolling the window through the cell. */
|
|
689
|
+
forecast(window: readonly (readonly number[])[]): { cpu: number; memory: number; io: number } {
|
|
690
|
+
if (window.length === 0) throw new Error('forecast requires at least one psi sample');
|
|
691
|
+
let hidden: Float64Array = this.#hidden;
|
|
692
|
+
for (const sample of window) hidden = this.#step(sample);
|
|
693
|
+
const out = [0, 0, 0];
|
|
694
|
+
for (let f = 0; f < this.#features; f += 1) {
|
|
695
|
+
let acc = 0;
|
|
696
|
+
for (let i = 0; i < this.#size; i += 1) acc += hidden[i] * this.#head[f * this.#size + i];
|
|
697
|
+
out[f] = Math.min(1, Math.max(0, acc));
|
|
698
|
+
}
|
|
699
|
+
return { cpu: out[0], memory: out[1], io: out[2] };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** truncated one-step online gradient towards the observed next value. */
|
|
703
|
+
trainstep(window: readonly (readonly number[])[], observed: readonly number[]): number {
|
|
704
|
+
const predicted = this.forecast(window);
|
|
705
|
+
const p = [predicted.cpu, predicted.memory, predicted.io];
|
|
706
|
+
let squared = 0;
|
|
707
|
+
for (let f = 0; f < this.#features; f += 1) {
|
|
708
|
+
const err = (observed[f] ?? 0) - p[f];
|
|
709
|
+
squared += err * err;
|
|
710
|
+
for (let i = 0; i < this.#size; i += 1) {
|
|
711
|
+
this.#head[f * this.#size + i] += 0.05 * err * this.#hidden[i];
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return Math.sqrt(squared / this.#features);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** resets the rolling state between unrelated series. */
|
|
718
|
+
reset(): void {
|
|
719
|
+
this.#hidden = new Float64Array(this.#size);
|
|
720
|
+
this.#cell = new Float64Array(this.#size);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/* ------------------------------------------------------------------ */
|
|
725
|
+
/* context: tenantqos (gold/silver/bronze tiers onto mttg cgroups) */
|
|
726
|
+
/* ------------------------------------------------------------------ */
|
|
727
|
+
|
|
728
|
+
/** the three external tenant tiers (mttg adds the internal idle class). */
|
|
729
|
+
export type tenanttier = 'gold' | 'silver' | 'bronze';
|
|
730
|
+
|
|
731
|
+
/** one compiled tier profile: cgroup writes plus scheduler policy. */
|
|
732
|
+
export interface tenantqosprofile {
|
|
733
|
+
readonly tier: tenanttier;
|
|
734
|
+
readonly cgroupwrites: readonly { readonly file: string; readonly content: string }[];
|
|
735
|
+
readonly cpumax: string;
|
|
736
|
+
readonly weight: number;
|
|
737
|
+
readonly timesliceboost: number;
|
|
738
|
+
readonly notes: readonly string[];
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/** tier table: weights follow the 1-10000 cgroups v2 kernel range. */
|
|
742
|
+
const tiertable: Readonly<Record<tenanttier, { weight: number; boost: number; cpus: number }>> = {
|
|
743
|
+
gold: { weight: 10000, boost: 2, cpus: 8 },
|
|
744
|
+
silver: { weight: 2500, boost: 1.25, cpus: 4 },
|
|
745
|
+
bronze: { weight: 100, boost: 1, cpus: 2 },
|
|
746
|
+
} as const;
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* compiles a gold/silver/bronze tier onto the mttg cgroups v2 builders
|
|
750
|
+
* of virtualization.ts: the tier maps through tenantqosmap (gold to
|
|
751
|
+
* guaranteed, silver to burstable, bronze to besteffort), the writes
|
|
752
|
+
* come from tenantcgroupbuilder.planwrites() and the cpu.max line from
|
|
753
|
+
* cpumaxqos(); the notes carry the EEVDF, BORE and sched_ext facts the
|
|
754
|
+
* kernel applies under each policy. mttg itself stays in
|
|
755
|
+
* virtualization.ts; this module only references it.
|
|
756
|
+
*/
|
|
757
|
+
export function tenantqosprofile(tier: tenanttier, memMaxMb = 2048): tenantqosprofile {
|
|
758
|
+
const entry = tiertable[tier];
|
|
759
|
+
const builder = new tenantcgroupbuilder(`tenant-${tier}`)
|
|
760
|
+
.withqos(tenantqosmap[tier])
|
|
761
|
+
.withweight(entry.weight)
|
|
762
|
+
.withcpus(Array.from({ length: entry.cpus }, (_, i) => i))
|
|
763
|
+
.withmemmax(memMaxMb);
|
|
764
|
+
return {
|
|
765
|
+
tier,
|
|
766
|
+
cgroupwrites: builder.planwrites(),
|
|
767
|
+
cpumax: cpumaxqos(tenantqosmap[tier], entry.cpus),
|
|
768
|
+
weight: entry.weight,
|
|
769
|
+
timesliceboost: entry.boost,
|
|
770
|
+
notes: [eevdfinfo(), boreinfo(), schedextinfo()],
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/* ------------------------------------------------------------------ */
|
|
775
|
+
/* context: admission (tier-aware queue with psi degradation) */
|
|
776
|
+
/* ------------------------------------------------------------------ */
|
|
777
|
+
|
|
778
|
+
/** one admission request pending a tier decision. */
|
|
779
|
+
export interface admissionrequest {
|
|
780
|
+
readonly id: string;
|
|
781
|
+
readonly tier: tenanttier;
|
|
782
|
+
readonly workloadName: string;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* tier-aware admission queue: submissions dequeue gold first, silver
|
|
787
|
+
* second and bronze last; when the psilstm forecast crosses the
|
|
788
|
+
* pressure threshold (any stall above 0.6) bronze submissions wait for
|
|
789
|
+
* one drain cycle, the degradation ladder the tenant QoS layer
|
|
790
|
+
* prescribes. the queue never rejects work, it only reorders and
|
|
791
|
+
* defers, so no request is ever lost.
|
|
792
|
+
*/
|
|
793
|
+
export class tenantadmission {
|
|
794
|
+
#queue: admissionrequest[] = [];
|
|
795
|
+
#forecast: { cpu: number; memory: number; io: number } = { cpu: 0, memory: 0, io: 0 };
|
|
796
|
+
|
|
797
|
+
submit(request: admissionrequest): number {
|
|
798
|
+
this.#queue.push(request);
|
|
799
|
+
return this.#queue.length;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** feeds the latest lstm forecast; true when degraded. */
|
|
803
|
+
observepressure(forecast: { cpu: number; memory: number; io: number }): boolean {
|
|
804
|
+
this.#forecast = forecast;
|
|
805
|
+
return Math.max(forecast.cpu, forecast.memory, forecast.io) > 0.6;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** dequeues in tier order; bronze defers under sustained pressure. */
|
|
809
|
+
admit(): admissionrequest | undefined {
|
|
810
|
+
const degraded = Math.max(this.#forecast.cpu, this.#forecast.memory, this.#forecast.io) > 0.6;
|
|
811
|
+
for (const tier of ['gold', 'silver', 'bronze'] as const) {
|
|
812
|
+
// bronze defers under pressure while higher tiers wait; the some()
|
|
813
|
+
// guard prevents starvation when bronze is the only queued tenant.
|
|
814
|
+
// the loop is the whole decision: when every eligible tier is empty
|
|
815
|
+
// (or bronze alone is deferred) admit returns undefined so the
|
|
816
|
+
// caller observes the deferral instead of silently draining head.
|
|
817
|
+
if (tier === 'bronze' && degraded && this.#queue.some((r) => r.tier !== 'bronze')) {
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
const index = this.#queue.findIndex((r) => r.tier === tier);
|
|
821
|
+
if (index >= 0) return this.#queue.splice(index, 1)[0];
|
|
822
|
+
}
|
|
823
|
+
return undefined;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
get pending(): number {
|
|
827
|
+
return this.#queue.length;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/* ------------------------------------------------------------------ */
|
|
832
|
+
/* context: anomaly (z-score + EWMA detector, OTLP OBI export) */
|
|
833
|
+
/* ------------------------------------------------------------------ */
|
|
834
|
+
|
|
835
|
+
/** one detected anomaly in the OpenTelemetry OBI (observability for
|
|
836
|
+
* intelligent agents) record shape. */
|
|
837
|
+
export interface anomalyrecord {
|
|
838
|
+
readonly series: string;
|
|
839
|
+
readonly value: number;
|
|
840
|
+
readonly zscore: number;
|
|
841
|
+
readonly ewma: number;
|
|
842
|
+
readonly severity: 'warning' | 'critical';
|
|
843
|
+
readonly ts: string;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/** one OTLP JSON log record (the exporter protocol shape). */
|
|
847
|
+
export interface otlplogrecord {
|
|
848
|
+
readonly timeUnixNano: string;
|
|
849
|
+
readonly severityText: string;
|
|
850
|
+
readonly body: { stringValue: string };
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* statistical anomaly detector following the OTel OBI pattern: each
|
|
855
|
+
* series keeps an exponentially weighted mean and variance (alpha
|
|
856
|
+
* 0.3), a value is anomalous when its z-score exceeds the 3.5
|
|
857
|
+
* iglewicz-hoaglin band, and findings serialize as OTLP JSON log
|
|
858
|
+
* records so any collector ingests them without an SDK dependency.
|
|
859
|
+
*/
|
|
860
|
+
export class anomalydetector {
|
|
861
|
+
#stats = new Map<string, { mean: number; variance: number; initialized: boolean }>();
|
|
862
|
+
#records: anomalyrecord[] = [];
|
|
863
|
+
|
|
864
|
+
observe(series: string, value: number): anomalyrecord | null {
|
|
865
|
+
const alpha = 0.3;
|
|
866
|
+
const state = this.#stats.get(series) ?? { mean: value, variance: 1e-6, initialized: false };
|
|
867
|
+
if (!state.initialized) {
|
|
868
|
+
/* first observation only seeds the baseline; nothing to compare. */
|
|
869
|
+
state.initialized = true;
|
|
870
|
+
this.#stats.set(series, state);
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
/* the z-score is tested against the prior baseline before the new
|
|
874
|
+
* value is absorbed, otherwise the anomaly inflates its own sd. */
|
|
875
|
+
const priormean = state.mean;
|
|
876
|
+
const priorsd = Math.sqrt(Math.max(state.variance, 1e-9));
|
|
877
|
+
const zscore = (value - priormean) / priorsd;
|
|
878
|
+
const delta = value - state.mean;
|
|
879
|
+
state.mean += alpha * delta;
|
|
880
|
+
state.variance = (1 - alpha) * (state.variance + alpha * delta * delta);
|
|
881
|
+
this.#stats.set(series, state);
|
|
882
|
+
if (Math.abs(zscore) <= 3.5) return null;
|
|
883
|
+
const record: anomalyrecord = {
|
|
884
|
+
series,
|
|
885
|
+
value,
|
|
886
|
+
zscore: Number(zscore.toFixed(3)),
|
|
887
|
+
ewma: Number(priormean.toFixed(4)),
|
|
888
|
+
severity: Math.abs(zscore) > 5 ? 'critical' : 'warning',
|
|
889
|
+
ts: new Date().toISOString(),
|
|
890
|
+
};
|
|
891
|
+
this.#records.push(record);
|
|
892
|
+
if (this.#records.length > 256) this.#records.shift();
|
|
893
|
+
return record;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/** OTLP JSON log-records payload for the otlp exporter protocol. */
|
|
897
|
+
exportotlp(): {
|
|
898
|
+
resourceLogs: {
|
|
899
|
+
resource: { attributes: { key: string; value: { stringValue: string } }[] };
|
|
900
|
+
scopeLogs: { scope: { name: string }; logRecords: readonly otlplogrecord[] }[];
|
|
901
|
+
}[];
|
|
902
|
+
} {
|
|
903
|
+
return {
|
|
904
|
+
resourceLogs: [
|
|
905
|
+
{
|
|
906
|
+
resource: {
|
|
907
|
+
attributes: [{ key: 'service.name', value: { stringValue: 'saddle-scheduler' } }],
|
|
908
|
+
},
|
|
909
|
+
scopeLogs: [
|
|
910
|
+
{
|
|
911
|
+
scope: { name: 'anomalydetector.obi' },
|
|
912
|
+
logRecords: this.#records.map((r) => ({
|
|
913
|
+
timeUnixNano: `${Date.parse(r.ts) * 1e6}`,
|
|
914
|
+
severityText: r.severity,
|
|
915
|
+
body: {
|
|
916
|
+
stringValue: `series=${r.series} value=${r.value} z=${r.zscore} ewma=${r.ewma}`,
|
|
917
|
+
},
|
|
918
|
+
})),
|
|
919
|
+
},
|
|
920
|
+
],
|
|
921
|
+
},
|
|
922
|
+
],
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
get count(): number {
|
|
927
|
+
return this.#records.length;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/* ------------------------------------------------------------------ */
|
|
932
|
+
/* context: hotreload (spec fragment reload bus) */
|
|
933
|
+
/* ------------------------------------------------------------------ */
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* watches spec fragments (cpu, gpu, memory, network json documents)
|
|
937
|
+
* and emits hashed reload events only when the content actually
|
|
938
|
+
* changed; the poll loop swallows reader failures so a broken fragment
|
|
939
|
+
* never kills the watcher.
|
|
940
|
+
*/
|
|
941
|
+
export class spechotreload extends EventEmitter {
|
|
942
|
+
#hashes = new Map<string, string>();
|
|
943
|
+
#timer: NodeJS.Timeout | null = null;
|
|
944
|
+
|
|
945
|
+
track(specName: string, content: string): boolean {
|
|
946
|
+
const hash = digest(content);
|
|
947
|
+
const previous = this.#hashes.get(specName);
|
|
948
|
+
this.#hashes.set(specName, hash);
|
|
949
|
+
if (previous !== undefined && previous !== hash) {
|
|
950
|
+
this.emit('reload', { specName, hash });
|
|
951
|
+
return true;
|
|
952
|
+
}
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/** starts a poll loop; safe to call repeatedly, stops via dispose. */
|
|
957
|
+
startpolling(pollMs: number, readspecs: () => Iterable<[string, string]>): void {
|
|
958
|
+
this.stoppolling();
|
|
959
|
+
this.#timer = setInterval(() => {
|
|
960
|
+
try {
|
|
961
|
+
for (const [name, content] of readspecs()) this.track(name, content);
|
|
962
|
+
} catch {
|
|
963
|
+
/* catcher: a reader failure must not kill the poll loop */
|
|
964
|
+
}
|
|
965
|
+
}, pollMs);
|
|
966
|
+
this.#timer.unref?.();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
stoppolling(): void {
|
|
970
|
+
if (this.#timer !== null) {
|
|
971
|
+
clearInterval(this.#timer);
|
|
972
|
+
this.#timer = null;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
[Symbol.dispose](): void {
|
|
977
|
+
this.stoppolling();
|
|
978
|
+
this.removeAllListeners();
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/* ------------------------------------------------------------------ */
|
|
983
|
+
/* context: otel (metrics bridge with OTLP shape) */
|
|
984
|
+
/* ------------------------------------------------------------------ */
|
|
985
|
+
|
|
986
|
+
/** one metric point in the OTLP metrics JSON shape. */
|
|
987
|
+
export interface otlpmetricpoint {
|
|
988
|
+
readonly name: string;
|
|
989
|
+
readonly kind: 'counter' | 'gauge' | 'histogram';
|
|
990
|
+
readonly value: number;
|
|
991
|
+
readonly attributes?: Readonly<Record<string, string>>;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* counters, gauges and histograms with OTLP-shaped export so the
|
|
996
|
+
* scheduler plugs into any OpenTelemetry collector without a dependency
|
|
997
|
+
* on the SDK; histograms collapse to sum plus count until explicit
|
|
998
|
+
* bucket boundaries are wired.
|
|
999
|
+
*/
|
|
1000
|
+
export class otelmetricsbridge {
|
|
1001
|
+
#points = new Map<string, otlpmetricpoint>();
|
|
1002
|
+
|
|
1003
|
+
#key(name: string, attributes?: Record<string, string>): string {
|
|
1004
|
+
return `${name}|${JSON.stringify(attributes ?? {})}`;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
counter(name: string, delta = 1, attributes?: Record<string, string>): void {
|
|
1008
|
+
const key = this.#key(name, attributes);
|
|
1009
|
+
const current = this.#points.get(key);
|
|
1010
|
+
this.#points.set(key, {
|
|
1011
|
+
name,
|
|
1012
|
+
kind: 'counter',
|
|
1013
|
+
value: (current?.kind === 'counter' ? current.value : 0) + delta,
|
|
1014
|
+
attributes,
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
gauge(name: string, value: number, attributes?: Record<string, string>): void {
|
|
1019
|
+
this.#points.set(this.#key(name, attributes), { name, kind: 'gauge', value, attributes });
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
histogram(name: string, value: number, attributes?: Record<string, string>): void {
|
|
1023
|
+
const key = this.#key(name, attributes);
|
|
1024
|
+
const current = this.#points.get(key);
|
|
1025
|
+
this.#points.set(key, {
|
|
1026
|
+
name,
|
|
1027
|
+
kind: 'histogram',
|
|
1028
|
+
value: (current?.kind === 'histogram' ? current.value : 0) + value,
|
|
1029
|
+
attributes,
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
export(): {
|
|
1034
|
+
resource: { 'service.name': string };
|
|
1035
|
+
scopeMetrics: { scope: { name: string }; metrics: readonly otlpmetricpoint[] };
|
|
1036
|
+
} {
|
|
1037
|
+
return {
|
|
1038
|
+
resource: { 'service.name': 'saddle-scheduler' },
|
|
1039
|
+
scopeMetrics: { scope: { name: 'saddle.scheduler' }, metrics: [...this.#points.values()] },
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/* ------------------------------------------------------------------ */
|
|
1045
|
+
/* context: health (liveness and readiness probes) */
|
|
1046
|
+
/* ------------------------------------------------------------------ */
|
|
1047
|
+
|
|
1048
|
+
/** aggregate health report with per-check durations. */
|
|
1049
|
+
export interface healthreport {
|
|
1050
|
+
readonly status: 'pass' | 'fail';
|
|
1051
|
+
readonly checks: readonly { name: string; ok: boolean; durationMs: number }[];
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* liveness and readiness probes with per-check timeouts; a failing
|
|
1056
|
+
* check never propagates, it only marks the aggregate status.
|
|
1057
|
+
*/
|
|
1058
|
+
export class healthcheckregistry {
|
|
1059
|
+
#checks = new Map<string, { kind: 'liveness' | 'readiness'; fn: () => Promise<void> | void }>();
|
|
1060
|
+
|
|
1061
|
+
register(name: string, kind: 'liveness' | 'readiness', fn: () => Promise<void> | void): this {
|
|
1062
|
+
this.#checks.set(name, { kind, fn });
|
|
1063
|
+
return this;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
async run(kind: 'liveness' | 'readiness', timeoutMs = 1000): Promise<healthreport> {
|
|
1067
|
+
const checks: { name: string; ok: boolean; durationMs: number }[] = [];
|
|
1068
|
+
for (const [name, check] of this.#checks) {
|
|
1069
|
+
if (check.kind !== kind) continue;
|
|
1070
|
+
const startedAt = Date.now();
|
|
1071
|
+
let ok = true;
|
|
1072
|
+
try {
|
|
1073
|
+
await Promise.race([
|
|
1074
|
+
Promise.resolve(check.fn()),
|
|
1075
|
+
new Promise((_resolve, reject) =>
|
|
1076
|
+
setTimeout(() => reject(new Error('timeout')), timeoutMs),
|
|
1077
|
+
),
|
|
1078
|
+
]);
|
|
1079
|
+
} catch {
|
|
1080
|
+
ok = false;
|
|
1081
|
+
}
|
|
1082
|
+
checks.push({ name, ok, durationMs: Date.now() - startedAt });
|
|
1083
|
+
}
|
|
1084
|
+
return { status: checks.every((c) => c.ok) ? 'pass' : 'fail', checks };
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/* ------------------------------------------------------------------ */
|
|
1089
|
+
/* context: shutdown (graceful LIFO coordinator) */
|
|
1090
|
+
/* ------------------------------------------------------------------ */
|
|
1091
|
+
|
|
1092
|
+
/** one shutdown task with its own timeout. */
|
|
1093
|
+
export interface shutdowntask {
|
|
1094
|
+
readonly name: string;
|
|
1095
|
+
readonly timeoutMs: number;
|
|
1096
|
+
run(): Promise<void>;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* runs shutdown tasks in LIFO order under a global deadline, the
|
|
1101
|
+
* sequence expected by SIGTERM-driven orchestrators (systemd,
|
|
1102
|
+
* kubernetes, fly machines); repeated arm calls are no-ops and a task
|
|
1103
|
+
* failure skips the task, never the chain.
|
|
1104
|
+
*/
|
|
1105
|
+
export class gracefulshutdown {
|
|
1106
|
+
#tasks: shutdowntask[] = [];
|
|
1107
|
+
#shuttingdown = false;
|
|
1108
|
+
|
|
1109
|
+
add(task: shutdowntask): this {
|
|
1110
|
+
this.#tasks.push(task);
|
|
1111
|
+
return this;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/** wires SIGTERM/SIGINT once. */
|
|
1115
|
+
arm(timeoutMs = 10_000): void {
|
|
1116
|
+
const handler = (): void => {
|
|
1117
|
+
void this.shutdown('signal', timeoutMs);
|
|
1118
|
+
};
|
|
1119
|
+
process.once('SIGTERM', handler);
|
|
1120
|
+
process.once('SIGINT', handler);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
async shutdown(
|
|
1124
|
+
reason: string,
|
|
1125
|
+
timeoutMs = 10_000,
|
|
1126
|
+
): Promise<{ reason: string; completed: readonly string[]; skipped: readonly string[] }> {
|
|
1127
|
+
this.#shuttingdown = true;
|
|
1128
|
+
const completed: string[] = [];
|
|
1129
|
+
const skipped: string[] = [];
|
|
1130
|
+
const deadline = Date.now() + timeoutMs;
|
|
1131
|
+
for (const task of this.#tasks.reverse()) {
|
|
1132
|
+
if (Date.now() >= deadline) {
|
|
1133
|
+
skipped.push(task.name);
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
try {
|
|
1137
|
+
await Promise.race([
|
|
1138
|
+
task.run(),
|
|
1139
|
+
new Promise((_resolve, reject) =>
|
|
1140
|
+
setTimeout(() => reject(new Error(`task ${task.name} timed out`)), task.timeoutMs),
|
|
1141
|
+
),
|
|
1142
|
+
]);
|
|
1143
|
+
completed.push(task.name);
|
|
1144
|
+
} catch {
|
|
1145
|
+
skipped.push(task.name);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
this.#tasks = [];
|
|
1149
|
+
this.#shuttingdown = false;
|
|
1150
|
+
return { reason, completed, skipped };
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
get inprogress(): boolean {
|
|
1154
|
+
return this.#shuttingdown;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/* ------------------------------------------------------------------ */
|
|
1159
|
+
/* context: hooks (typed plugin lifecycle chain) */
|
|
1160
|
+
/* ------------------------------------------------------------------ */
|
|
1161
|
+
|
|
1162
|
+
/** lifecycle events a plugin may observe. */
|
|
1163
|
+
export type pluginevent =
|
|
1164
|
+
| 'tenant.create'
|
|
1165
|
+
| 'tenant.admit'
|
|
1166
|
+
| 'allocation.change'
|
|
1167
|
+
| 'spec.reload'
|
|
1168
|
+
| 'scheduler.telemetry';
|
|
1169
|
+
|
|
1170
|
+
/** one plugin handler. */
|
|
1171
|
+
export type pluginhandler = (payload: unknown) => Promise<void> | void;
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* typed lifecycle hooks (tenant create/admit, allocation change, spec
|
|
1175
|
+
* reload, telemetry) with isolated failure handling: one plugin error
|
|
1176
|
+
* never aborts the chain, it only counts as a failed handler.
|
|
1177
|
+
*/
|
|
1178
|
+
export class pluginhooksystem implements Disposable {
|
|
1179
|
+
#handlers = new Map<pluginevent, Set<pluginhandler>>();
|
|
1180
|
+
|
|
1181
|
+
on(event: pluginevent, handler: pluginhandler): this {
|
|
1182
|
+
const set = this.#handlers.get(event) ?? new Set<pluginhandler>();
|
|
1183
|
+
set.add(handler);
|
|
1184
|
+
this.#handlers.set(event, set);
|
|
1185
|
+
return this;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
async emit(event: pluginevent, payload: unknown): Promise<{ handled: number; failed: number }> {
|
|
1189
|
+
let handled = 0;
|
|
1190
|
+
let failed = 0;
|
|
1191
|
+
for (const handler of this.#handlers.get(event) ?? []) {
|
|
1192
|
+
try {
|
|
1193
|
+
await handler(payload);
|
|
1194
|
+
handled += 1;
|
|
1195
|
+
} catch {
|
|
1196
|
+
failed += 1;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return { handled, failed };
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
[Symbol.dispose](): void {
|
|
1203
|
+
this.#handlers.clear();
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/* ------------------------------------------------------------------ */
|
|
1208
|
+
/* context: featureledger (scheduling domain feature index) */
|
|
1209
|
+
/* ------------------------------------------------------------------ */
|
|
1210
|
+
|
|
1211
|
+
/** origin of one feature: the v2 future.ts ledger or the v3 additions. */
|
|
1212
|
+
export type schedulerfeatureorigin = 'future.ts' | 'omnihypercore-v3';
|
|
1213
|
+
|
|
1214
|
+
/** one ledger row: id, origin, name and export name. */
|
|
1215
|
+
export interface schedulerfeaturemeta {
|
|
1216
|
+
readonly id: string;
|
|
1217
|
+
readonly origin: schedulerfeatureorigin;
|
|
1218
|
+
readonly name: string;
|
|
1219
|
+
readonly exportname: string;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* the scheduling-domain ledger: fourteen features absorbed from the
|
|
1224
|
+
* removed future.ts (ts7 020-022, hardware allocation 039-044,
|
|
1225
|
+
* developer experience 051-055) plus four omnihypercore additions. the
|
|
1226
|
+
* checkpoint family (028-032) moved to virtualization.ts, the
|
|
1227
|
+
* post-quantum family (023-027) to security.ts and the remaining
|
|
1228
|
+
* twenty-five compute-oriented features to compute.ts.
|
|
1229
|
+
*/
|
|
1230
|
+
export const schedulerfeatureindex: readonly schedulerfeaturemeta[] = [
|
|
1231
|
+
{
|
|
1232
|
+
id: '020',
|
|
1233
|
+
origin: 'future.ts',
|
|
1234
|
+
name: 'parallel typecheck shards',
|
|
1235
|
+
exportname: 'plantypecheckshards',
|
|
1236
|
+
},
|
|
1237
|
+
{ id: '021', origin: 'future.ts', name: 'shared-memory cache', exportname: 'sharedmemorycache' },
|
|
1238
|
+
{ id: '022', origin: 'future.ts', name: 'incremental project graph', exportname: 'projectgraph' },
|
|
1239
|
+
{ id: '039', origin: 'future.ts', name: 'MIG slicing profiles', exportname: 'planmiglayout' },
|
|
1240
|
+
{ id: '040', origin: 'future.ts', name: 'vCPU hotplug 1-192', exportname: 'vcpuhotplug' },
|
|
1241
|
+
{ id: '041', origin: 'future.ts', name: 'RAM hotplug 1-1024GB', exportname: 'ramhotplug' },
|
|
1242
|
+
{ id: '042', origin: 'future.ts', name: 'multi-GPU composition', exportname: 'plancomposition' },
|
|
1243
|
+
{
|
|
1244
|
+
id: '043',
|
|
1245
|
+
origin: 'future.ts',
|
|
1246
|
+
name: 'NUMA layout with distances',
|
|
1247
|
+
exportname: 'planumalayout',
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
id: '044',
|
|
1251
|
+
origin: 'future.ts',
|
|
1252
|
+
name: 'PCI device model registry',
|
|
1253
|
+
exportname: 'devicemodelregistry',
|
|
1254
|
+
},
|
|
1255
|
+
{ id: '051', origin: 'future.ts', name: 'spec hot reload bus', exportname: 'spechotreload' },
|
|
1256
|
+
{
|
|
1257
|
+
id: '052',
|
|
1258
|
+
origin: 'future.ts',
|
|
1259
|
+
name: 'OpenTelemetry metrics bridge',
|
|
1260
|
+
exportname: 'otelmetricsbridge',
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
id: '053',
|
|
1264
|
+
origin: 'future.ts',
|
|
1265
|
+
name: 'health check registry',
|
|
1266
|
+
exportname: 'healthcheckregistry',
|
|
1267
|
+
},
|
|
1268
|
+
{ id: '054', origin: 'future.ts', name: 'graceful shutdown', exportname: 'gracefulshutdown' },
|
|
1269
|
+
{ id: '055', origin: 'future.ts', name: 'plugin hook system', exportname: 'pluginhooksystem' },
|
|
1270
|
+
{
|
|
1271
|
+
id: 'n56',
|
|
1272
|
+
origin: 'omnihypercore-v3',
|
|
1273
|
+
name: 'aipsched micro-scheduler',
|
|
1274
|
+
exportname: 'aipsched',
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
id: 'n57',
|
|
1278
|
+
origin: 'omnihypercore-v3',
|
|
1279
|
+
name: 'psi LSTM pressure predictor',
|
|
1280
|
+
exportname: 'psilstm',
|
|
1281
|
+
},
|
|
1282
|
+
{
|
|
1283
|
+
id: 'n58',
|
|
1284
|
+
origin: 'omnihypercore-v3',
|
|
1285
|
+
name: 'tenant QoS gold/silver/bronze',
|
|
1286
|
+
exportname: 'tenantqosprofile',
|
|
1287
|
+
},
|
|
1288
|
+
{
|
|
1289
|
+
id: 'n59',
|
|
1290
|
+
origin: 'omnihypercore-v3',
|
|
1291
|
+
name: 'OTel OBI anomaly detector',
|
|
1292
|
+
exportname: 'anomalydetector',
|
|
1293
|
+
},
|
|
1294
|
+
] satisfies readonly schedulerfeaturemeta[];
|
|
1295
|
+
|
|
1296
|
+
/** returns the ledger count split by origin. */
|
|
1297
|
+
export function schedulerfeaturecount(): { total: number; absorbed: number; added: number } {
|
|
1298
|
+
const absorbed = schedulerfeatureindex.filter((f) => f.origin === 'future.ts').length;
|
|
1299
|
+
return {
|
|
1300
|
+
total: schedulerfeatureindex.length,
|
|
1301
|
+
absorbed,
|
|
1302
|
+
added: schedulerfeatureindex.length - absorbed,
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/** version anchors of the scheduling domain (worklog v3-VERIFY). */
|
|
1307
|
+
export const schedulerversions = {
|
|
1308
|
+
kernelmainline: '7.2',
|
|
1309
|
+
kernelstable: '7.1.9',
|
|
1310
|
+
kernellongterm: '6.18.45',
|
|
1311
|
+
eevdfsince: '6.6',
|
|
1312
|
+
schedextsince: '6.12',
|
|
1313
|
+
hostparallelism: `${availableParallelism?.() ?? 1}`,
|
|
1314
|
+
} as const satisfies Record<string, string>;
|
|
1315
|
+
|
|
1316
|
+
/* ------------------------------------------------------------------ */
|
|
1317
|
+
/* context: v5-C feature audit builders (ledger F-053 and F-063 of */
|
|
1318
|
+
/* docs/viability.md appendices A and B) */
|
|
1319
|
+
/* ------------------------------------------------------------------ */
|
|
1320
|
+
|
|
1321
|
+
/** the autoscaling arms the bandit chooses from. */
|
|
1322
|
+
export type autoscalearm = 'hold' | 'up1' | 'up2' | 'down1';
|
|
1323
|
+
|
|
1324
|
+
/** one observed reward sample and the decision that produced it. */
|
|
1325
|
+
export interface autoscalerdecision {
|
|
1326
|
+
readonly arm: autoscalearm;
|
|
1327
|
+
readonly explored: boolean;
|
|
1328
|
+
readonly epsilon: number;
|
|
1329
|
+
readonly expectedreward: number;
|
|
1330
|
+
readonly replicas: number;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* the reinforcement-learning autoscaler policy (ledger F-053): a
|
|
1335
|
+
* functional epsilon-greedy k-armed bandit over four scaling arms
|
|
1336
|
+
* (hold, +1, +2, -1 replicas). rewards are the negated absolute error
|
|
1337
|
+
* between the achieved load and the target load after an arm fires, so
|
|
1338
|
+
* maximizing reward equals minimizing oscillation around the setpoint.
|
|
1339
|
+
* action values update with an incremental mean, epsilon decays toward
|
|
1340
|
+
* a floor as confidence grows, and replica counts are clamped to the
|
|
1341
|
+
* configured bounds; no external stable-baselines3 dependency is
|
|
1342
|
+
* needed because the bandit is the part of RL that matters here.
|
|
1343
|
+
*/
|
|
1344
|
+
export class rlautoscaler {
|
|
1345
|
+
readonly #q = new Map<autoscalearm, { value: number; count: number }>();
|
|
1346
|
+
readonly #arms: readonly autoscalearm[];
|
|
1347
|
+
readonly #floor: number;
|
|
1348
|
+
readonly #decay: number;
|
|
1349
|
+
#epsilon: number;
|
|
1350
|
+
#replicas: number;
|
|
1351
|
+
#lastarm: autoscalearm | undefined;
|
|
1352
|
+
|
|
1353
|
+
constructor(
|
|
1354
|
+
opts: {
|
|
1355
|
+
replicas?: number;
|
|
1356
|
+
minreplicas?: number;
|
|
1357
|
+
maxreplicas?: number;
|
|
1358
|
+
epsilon?: number;
|
|
1359
|
+
epsilonfloor?: number;
|
|
1360
|
+
decay?: number;
|
|
1361
|
+
} = {},
|
|
1362
|
+
) {
|
|
1363
|
+
try {
|
|
1364
|
+
this.#replicas = opts.replicas ?? 2;
|
|
1365
|
+
this.#decay = opts.decay ?? 0.98;
|
|
1366
|
+
this.#epsilon = opts.epsilon ?? 0.2;
|
|
1367
|
+
if (!Number.isInteger(this.#replicas) || this.#replicas < 1 || this.#replicas > 4096) {
|
|
1368
|
+
throw new Error('replicas must be an integer 1..4096');
|
|
1369
|
+
}
|
|
1370
|
+
if (this.#epsilon < 0 || this.#epsilon > 1) throw new Error('epsilon must be 0..1');
|
|
1371
|
+
this.#floor = Math.min(opts.epsilonfloor ?? 0.05, this.#epsilon);
|
|
1372
|
+
if (this.#floor < 0) throw new Error('epsilon floor must be >= 0');
|
|
1373
|
+
if (this.#decay <= 0 || this.#decay >= 1) throw new Error('decay must be in (0,1)');
|
|
1374
|
+
const min = opts.minreplicas ?? 1;
|
|
1375
|
+
const max = opts.maxreplicas ?? 64;
|
|
1376
|
+
if (min < 1 || max < min) throw new Error('max replicas must be >= min replicas >= 1');
|
|
1377
|
+
this.#arms = ['hold', 'up1', 'up2', 'down1'];
|
|
1378
|
+
for (const arm of this.#arms) this.#q.set(arm, { value: 0, count: 0 });
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
throw new Error(
|
|
1381
|
+
`rlautoscaler init failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/** current replica count after the last applied decision. */
|
|
1387
|
+
get replicas(): number {
|
|
1388
|
+
return this.#replicas;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/** current exploration rate (decays toward the floor). */
|
|
1392
|
+
get epsilon(): number {
|
|
1393
|
+
return Number(this.#epsilon.toFixed(4));
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/** selects the next arm: explore with probability epsilon, else greedy. */
|
|
1397
|
+
decide(targetload: number, observedload: number): autoscalerdecision {
|
|
1398
|
+
try {
|
|
1399
|
+
if (targetload <= 0 || targetload > 1) throw new Error('target load must be in (0,1]');
|
|
1400
|
+
if (observedload < 0 || observedload > 1) throw new Error('observed load must be in [0,1]');
|
|
1401
|
+
const explore = Math.random() < this.#epsilon;
|
|
1402
|
+
const best = [...this.#q.entries()].sort((a, b) => b[1].value - a[1].value)[0][0];
|
|
1403
|
+
const arm: autoscalearm = explore
|
|
1404
|
+
? this.#arms[Math.floor(Math.random() * this.#arms.length)]
|
|
1405
|
+
: best;
|
|
1406
|
+
this.#lastarm = arm;
|
|
1407
|
+
if (arm === 'up1') this.#replicas += 1;
|
|
1408
|
+
else if (arm === 'up2') this.#replicas += 2;
|
|
1409
|
+
else if (arm === 'down1') this.#replicas = Math.max(1, this.#replicas - 1);
|
|
1410
|
+
return {
|
|
1411
|
+
arm,
|
|
1412
|
+
explored: explore,
|
|
1413
|
+
epsilon: this.epsilon,
|
|
1414
|
+
expectedreward: Number((this.#q.get(arm)?.value ?? 0).toFixed(4)),
|
|
1415
|
+
replicas: this.#replicas,
|
|
1416
|
+
};
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
throw new Error(
|
|
1419
|
+
`rlautoscaler.decide failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/**
|
|
1425
|
+
* feeds the post-decision load back: reward is minus the distance to
|
|
1426
|
+
* the target (so 0 is perfect), the arm's value updates by
|
|
1427
|
+
* incremental mean and epsilon decays one step toward the floor.
|
|
1428
|
+
*/
|
|
1429
|
+
feedback(targetload: number, observedload: number): number {
|
|
1430
|
+
try {
|
|
1431
|
+
const arm = this.#lastarm;
|
|
1432
|
+
if (arm === undefined) throw new Error('no decision to reward yet');
|
|
1433
|
+
if (observedload < 0 || observedload > 1) throw new Error('observed load must be in [0,1]');
|
|
1434
|
+
if (targetload <= 0 || targetload > 1) throw new Error('target load must be in (0,1]');
|
|
1435
|
+
const reward = -Math.abs(observedload - targetload);
|
|
1436
|
+
const slot = this.#q.get(arm);
|
|
1437
|
+
if (slot === undefined) throw new Error(`unknown arm ${arm}`);
|
|
1438
|
+
slot.count += 1;
|
|
1439
|
+
slot.value += (reward - slot.value) / slot.count;
|
|
1440
|
+
this.#epsilon = Math.max(this.#floor, this.#epsilon * this.#decay);
|
|
1441
|
+
this.#lastarm = undefined;
|
|
1442
|
+
return Number(reward.toFixed(4));
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
throw new Error(
|
|
1445
|
+
`rlautoscaler.feedback failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
/** one replicated vm.config field: value plus a hybrid logical clock. */
|
|
1452
|
+
export interface crdtentry {
|
|
1453
|
+
readonly key: string;
|
|
1454
|
+
readonly value: string;
|
|
1455
|
+
readonly site: string;
|
|
1456
|
+
readonly wallms: number;
|
|
1457
|
+
readonly counter: number;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/** the result of merging a remote document into a local one. */
|
|
1461
|
+
export interface crdtmergeresult {
|
|
1462
|
+
readonly applied: number;
|
|
1463
|
+
readonly keptlocal: number;
|
|
1464
|
+
readonly converged: boolean;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* the distributed vm.config document CRDT (ledger F-063): a minimal
|
|
1469
|
+
* last-writer-wins element-register map in the Automerge/Yjs spirit
|
|
1470
|
+
* with zero dependencies. every field carries a site id plus a hybrid
|
|
1471
|
+
* logical clock (wall clock in ms, monotonic counter, lexicographic
|
|
1472
|
+
* site tiebreak) so concurrent edits converge deterministically on all
|
|
1473
|
+
* replicas; merge is commutative, associative and idempotent, and
|
|
1474
|
+
* todocument reassembles the converged vm.config object. the generic
|
|
1475
|
+
* key-value transport used to ship deltas between peers lives in
|
|
1476
|
+
* compute.ts (statevectorsync); this class owns the document shape.
|
|
1477
|
+
*/
|
|
1478
|
+
export class vmconfigcrdt {
|
|
1479
|
+
readonly #store = new Map<string, crdtentry>();
|
|
1480
|
+
readonly #site: string;
|
|
1481
|
+
#lastms = 0;
|
|
1482
|
+
#counter = 0;
|
|
1483
|
+
|
|
1484
|
+
constructor(site: string) {
|
|
1485
|
+
try {
|
|
1486
|
+
if (!/^[a-z][a-z0-9-]*$/.test(site)) {
|
|
1487
|
+
throw new Error('site id must be lowercase alnum/dash');
|
|
1488
|
+
}
|
|
1489
|
+
this.#site = site;
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
throw new Error(
|
|
1492
|
+
`vmconfigcrdt init failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/** site id of this replica. */
|
|
1498
|
+
get site(): string {
|
|
1499
|
+
return this.#site;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* writes one field with a fresh hybrid logical clock: the wall clock
|
|
1504
|
+
* dominates, the counter breaks same-millisecond ties, the site id
|
|
1505
|
+
* breaks identical clocks (deterministic across replicas).
|
|
1506
|
+
*/
|
|
1507
|
+
set(key: string, value: string): crdtentry {
|
|
1508
|
+
try {
|
|
1509
|
+
if (!/^[a-z][a-z0-9.]*$/.test(key)) {
|
|
1510
|
+
throw new Error(`key ${key} must be lowercase alnum/dot (vm.config sections)`);
|
|
1511
|
+
}
|
|
1512
|
+
if (value.length > 4096) throw new Error('value exceeds the 4 KB field budget');
|
|
1513
|
+
const now = Date.now();
|
|
1514
|
+
this.#counter = now === this.#lastms ? this.#counter + 1 : 0;
|
|
1515
|
+
this.#lastms = now;
|
|
1516
|
+
const entry: crdtentry = {
|
|
1517
|
+
key,
|
|
1518
|
+
value,
|
|
1519
|
+
site: this.#site,
|
|
1520
|
+
wallms: now,
|
|
1521
|
+
counter: this.#counter,
|
|
1522
|
+
};
|
|
1523
|
+
const current = this.#store.get(key);
|
|
1524
|
+
if (current === undefined || wins(entry, current)) this.#store.set(key, entry);
|
|
1525
|
+
return entry;
|
|
1526
|
+
} catch (error) {
|
|
1527
|
+
throw new Error(
|
|
1528
|
+
`vmconfigcrdt.set failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/** reads one converged field (undefined when absent). */
|
|
1534
|
+
get(key: string): crdtentry | undefined {
|
|
1535
|
+
return this.#store.get(key);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
/** the full delta set a remote peer needs (all entries). */
|
|
1539
|
+
snapshot(): readonly crdtentry[] {
|
|
1540
|
+
return [...this.#store.values()];
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
/**
|
|
1544
|
+
* applies remote entries with last-writer-wins semantics and reports
|
|
1545
|
+
* whether both replicas now hold the identical store (convergence).
|
|
1546
|
+
*/
|
|
1547
|
+
merge(remote: readonly crdtentry[], remotesite?: vmconfigcrdt): crdtmergeresult {
|
|
1548
|
+
try {
|
|
1549
|
+
let applied = 0;
|
|
1550
|
+
let keptlocal = 0;
|
|
1551
|
+
for (const entry of remote) {
|
|
1552
|
+
const current = this.#store.get(entry.key);
|
|
1553
|
+
if (current === undefined || wins(entry, current)) {
|
|
1554
|
+
this.#store.set(entry.key, entry);
|
|
1555
|
+
applied += 1;
|
|
1556
|
+
} else {
|
|
1557
|
+
keptlocal += 1;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
const converged =
|
|
1561
|
+
remotesite === undefined
|
|
1562
|
+
? true
|
|
1563
|
+
: this.#samekeys(remotesite) &&
|
|
1564
|
+
[...this.#store.values()].every((local) => {
|
|
1565
|
+
const remoteentry = remotesite.get(local.key);
|
|
1566
|
+
return remoteentry !== undefined && remoteentry.value === local.value;
|
|
1567
|
+
});
|
|
1568
|
+
return { applied, keptlocal, converged };
|
|
1569
|
+
} catch (error) {
|
|
1570
|
+
throw new Error(
|
|
1571
|
+
`vmconfigcrdt.merge failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* reassembles the converged vm.config document as a plain record,
|
|
1578
|
+
* ready for JSON.stringify (the distributed vm.config.json itself).
|
|
1579
|
+
*/
|
|
1580
|
+
todocument(): Record<string, string> {
|
|
1581
|
+
const doc: Record<string, string> = {};
|
|
1582
|
+
for (const [key, entry] of this.#store) doc[key] = entry.value;
|
|
1583
|
+
return doc;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
#samekeys(other: vmconfigcrdt): boolean {
|
|
1587
|
+
if (other.#store.size !== this.#store.size) return false;
|
|
1588
|
+
for (const key of this.#store.keys()) {
|
|
1589
|
+
if (!other.#store.has(key)) return false;
|
|
1590
|
+
}
|
|
1591
|
+
return true;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
/** true when candidate beats incumbent under the hlc ordering rule. */
|
|
1596
|
+
function wins(candidate: crdtentry, incumbent: crdtentry): boolean {
|
|
1597
|
+
if (candidate.wallms !== incumbent.wallms) return candidate.wallms > incumbent.wallms;
|
|
1598
|
+
if (candidate.counter !== incumbent.counter) return candidate.counter > incumbent.counter;
|
|
1599
|
+
return candidate.site > incumbent.site;
|
|
1600
|
+
}
|