@aiwg/cli 2026.10.1 → 2026.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/agentic/code/providers/capability-matrix.yaml +270 -0
- package/agentic/code/providers/model-capabilities.v1.json +3183 -24
- package/bin/aiwg.mjs +135 -47
- package/dist/src/a2a/codecs.js +9 -0
- package/dist/src/agents/grounding/knowledge-base.js +54 -6
- package/dist/src/artifacts/browser-export.js +6 -0
- package/dist/src/artifacts/checksum-manifest.js +1 -0
- package/dist/src/artifacts/citation-parser.js +173 -92
- package/dist/src/artifacts/citation-table-schema.js +59 -0
- package/dist/src/artifacts/cli.js +57 -18
- package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
- package/dist/src/artifacts/corpus-tools/vision-extract.js +107 -15
- package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
- package/dist/src/artifacts/corpus-views/renderers.js +3 -3
- package/dist/src/artifacts/discovery-eval.js +6 -4
- package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
- package/dist/src/artifacts/fortemi-core-sync.js +166 -40
- package/dist/src/artifacts/framework-capabilities.js +171 -0
- package/dist/src/artifacts/framework-index-paths.js +73 -0
- package/dist/src/artifacts/framework-source-digest.js +75 -0
- package/dist/src/artifacts/index-builder.js +285 -131
- package/dist/src/artifacts/index-files.js +132 -18
- package/dist/src/artifacts/index-preparation.js +65 -0
- package/dist/src/artifacts/index-refresh.js +576 -0
- package/dist/src/artifacts/index-worker.mjs +9 -0
- package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
- package/dist/src/artifacts/prebuilt-deployment.js +46 -0
- package/dist/src/artifacts/prebuilt-relations.js +80 -0
- package/dist/src/artifacts/query-engine.js +410 -88
- package/dist/src/artifacts/relations-builder.js +168 -0
- package/dist/src/artifacts/relations.js +148 -0
- package/dist/src/artifacts/repair.js +9 -4
- package/dist/src/artifacts/stats.js +5 -2
- package/dist/src/artifacts/types.js +30 -10
- package/dist/src/artifacts/watcher.js +1 -1
- package/dist/src/auth/credential-store.js +2 -1
- package/dist/src/catalog/cli.d.mts +5 -0
- package/dist/src/channel/manager.d.mts +20 -0
- package/dist/src/channel/manager.mjs +78 -12
- package/dist/src/cli/child-process-result.js +40 -0
- package/dist/src/cli/command-log.js +2 -3
- package/dist/src/cli/env.js +38 -16
- package/dist/src/cli/flag-parsing.js +236 -0
- package/dist/src/cli/handlers/artifacts.js +3 -0
- package/dist/src/cli/handlers/auth.js +3 -0
- package/dist/src/cli/handlers/decision.js +218 -5
- package/dist/src/cli/handlers/diagnose.js +13 -5
- package/dist/src/cli/handlers/effect.js +4 -2
- package/dist/src/cli/handlers/evidence.js +3 -0
- package/dist/src/cli/handlers/feedback.js +14 -5
- package/dist/src/cli/handlers/help.js +1 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/init.js +248 -30
- package/dist/src/cli/handlers/lint.js +5 -0
- package/dist/src/cli/handlers/mc.js +23 -0
- package/dist/src/cli/handlers/models.js +169 -10
- package/dist/src/cli/handlers/ralph-launcher.js +2 -0
- package/dist/src/cli/handlers/ralph.js +3 -0
- package/dist/src/cli/handlers/refresh.js +28 -0
- package/dist/src/cli/handlers/regenerate.js +14 -0
- package/dist/src/cli/handlers/resource-versions.js +3 -0
- package/dist/src/cli/handlers/rules.js +195 -0
- package/dist/src/cli/handlers/run.js +4 -0
- package/dist/src/cli/handlers/runtime-info.js +25 -11
- package/dist/src/cli/handlers/script-runner.js +19 -8
- package/dist/src/cli/handlers/session.js +406 -62
- package/dist/src/cli/handlers/sessions.js +243 -76
- package/dist/src/cli/handlers/setup-manifest.js +474 -79
- package/dist/src/cli/handlers/setup.js +180 -39
- package/dist/src/cli/handlers/steward.js +11 -0
- package/dist/src/cli/handlers/subcommands.js +60 -3
- package/dist/src/cli/handlers/team.js +18 -8
- package/dist/src/cli/handlers/uhp.js +3 -0
- package/dist/src/cli/handlers/use.js +307 -55
- package/dist/src/cli/handlers/utilities.js +125 -26
- package/dist/src/cli/handlers/version.js +8 -0
- package/dist/src/cli/prompt-utils.js +72 -0
- package/dist/src/cli/provider-resolution.js +11 -0
- package/dist/src/cli/router.js +15 -1
- package/dist/src/cli/services/deployment-verification.js +19 -9
- package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
- package/dist/src/cli/skill-usage.js +2 -3
- package/dist/src/config/aiwg-config.js +349 -35
- package/dist/src/config/cli.js +256 -23
- package/dist/src/config/decision-config.js +106 -0
- package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
- package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
- package/dist/src/config/gitignore.js +50 -0
- package/dist/src/config/index-graph-overrides.js +36 -0
- package/dist/src/config/project-artifacts-health.d.mts +37 -0
- package/dist/src/config/project-artifacts-health.mjs +14 -3
- package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
- package/dist/src/config/user-config-dir.d.mts +9 -0
- package/dist/src/config/user-config.js +38 -11
- package/dist/src/config/user-registry.js +23 -15
- package/dist/src/config/user-state.js +7 -0
- package/dist/src/decision/adapters/jev.js +75 -27
- package/dist/src/decision/ask.js +203 -43
- package/dist/src/decision/context-live-qualification.js +2 -2
- package/dist/src/decision/driver.js +14 -7
- package/dist/src/decision/ensemble/aggregate.js +23 -10
- package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
- package/dist/src/decision/entry.js +25 -6
- package/dist/src/decision/graph-live-qualification.js +1 -1
- package/dist/src/decision/graph-run.js +9 -0
- package/dist/src/decision/heldout/collector.js +15 -14
- package/dist/src/decision/heldout/contract.js +30 -7
- package/dist/src/decision/index.js +1 -0
- package/dist/src/decision/interop.js +60 -8
- package/dist/src/decision/jev-credentials.js +15 -1
- package/dist/src/decision/ledger.js +942 -0
- package/dist/src/decision/offload-activation.js +137 -0
- package/dist/src/decision/offload.js +550 -0
- package/dist/src/decision/qualification/load.js +4 -6
- package/dist/src/decision/savings.js +28 -0
- package/dist/src/decision/validate.js +6 -3
- package/dist/src/effects/dispatch-verifier.js +252 -0
- package/dist/src/effects/index.js +1 -0
- package/dist/src/effects/types.js +5 -1
- package/dist/src/effects/verifiers/git.js +2 -1
- package/dist/src/effects/verifiers/tracker.js +26 -14
- package/dist/src/extensions/commands/definitions.js +19 -0
- package/dist/src/extensions/project-local-remove.js +36 -1
- package/dist/src/features/installer.js +2 -2
- package/dist/src/flow/graph-metadata.js +5 -0
- package/dist/src/gates/providers/loader.js +64 -18
- package/dist/src/installation/manager-command.d.mts +18 -0
- package/dist/src/installation/manager-command.mjs +15 -3
- package/dist/src/installation/manager.d.mts +49 -0
- package/dist/src/installation/manager.mjs +95 -5
- package/dist/src/issues/cli.js +4 -4
- package/dist/src/issues/local.js +24 -5
- package/dist/src/jobs/runner.js +1 -1
- package/dist/src/lint/cli.js +27 -1
- package/dist/src/lint/loader.js +2 -1
- package/dist/src/lint/reporters.js +25 -1
- package/dist/src/lint/rule-activation.js +43 -0
- package/dist/src/lint/runner.js +137 -62
- package/dist/src/mcp/adapters/codex-runtime.js +366 -56
- package/dist/src/mcp/claude-install.d.mts +4 -0
- package/dist/src/mcp/claude-install.mjs +41 -0
- package/dist/src/mcp/cli.d.mts +1 -0
- package/dist/src/mcp/cli.mjs +620 -85
- package/dist/src/mcp/config-file.d.mts +28 -0
- package/dist/src/mcp/config-file.mjs +279 -0
- package/dist/src/mcp/config-layers.d.mts +36 -0
- package/dist/src/mcp/config-layers.mjs +348 -0
- package/dist/src/mcp/credentials.d.mts +51 -0
- package/dist/src/mcp/credentials.mjs +484 -0
- package/dist/src/mcp/grok-build-config.d.mts +6 -0
- package/dist/src/mcp/grok-build-config.mjs +17 -27
- package/dist/src/mcp/helpers.mjs +109 -4
- package/dist/src/mcp/muse-native-config.d.mts +13 -0
- package/dist/src/mcp/muse-native-config.mjs +1 -0
- package/dist/src/mcp/omp-config.d.mts +4 -0
- package/dist/src/mcp/omp-config.mjs +50 -29
- package/dist/src/mcp/profiles.js +97 -23
- package/dist/src/mcp/profiles.mjs +99 -27
- package/dist/src/mcp/registry.js +248 -131
- package/dist/src/mcp/registry.mjs +169 -100
- package/dist/src/mcp/server-config.d.mts +3 -0
- package/dist/src/mcp/server-config.mjs +89 -0
- package/dist/src/mcp/server.mjs +127 -82
- package/dist/src/mcp/test-simple.mjs +2 -1
- package/dist/src/mcp/toml-editor.d.mts +4 -0
- package/dist/src/mcp/toml-string.d.mts +1 -0
- package/dist/src/mcp/toml-string.mjs +9 -0
- package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
- package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
- package/dist/src/mcp/tool-filters.d.mts +46 -0
- package/dist/src/mcp/tool-filters.mjs +357 -0
- package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
- package/dist/src/mcp/tools/decision.mjs +22 -3
- package/dist/src/mcp/tools/discovery.mjs +62 -55
- package/dist/src/mcp/tools/orchestration.mjs +50 -2
- package/dist/src/mcp/tools/subsystems.mjs +39 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +2856 -0
- package/dist/src/mcp/tools/worker-shell-control.mjs +289 -0
- package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
- package/dist/src/memory/project-registry.js +2 -2
- package/dist/src/mission-protocol/codecs.js +647 -1
- package/dist/src/mission-protocol/types.js +110 -0
- package/dist/src/mission-protocol/worker-status.js +2 -0
- package/dist/src/models/config-loader.js +76 -14
- package/dist/src/models/grok-build-models.js +93 -41
- package/dist/src/models/model-capabilities.v1.json +3183 -24
- package/dist/src/models/model-discovery.js +931 -67
- package/dist/src/models/provider-policy.js +719 -11
- package/dist/src/models/qualification-digest.d.mts +2 -0
- package/dist/src/models/qualification-digest.mjs +17 -0
- package/dist/src/models/resolver.js +261 -0
- package/dist/src/models/worker-direct-readiness.js +550 -0
- package/dist/src/models/worker-dispatch-offer.js +169 -0
- package/dist/src/models/worker-health-readiness.js +144 -0
- package/dist/src/models/worker-installation.d.mts +39 -0
- package/dist/src/models/worker-installation.mjs +986 -0
- package/dist/src/models/wrapper-route.js +18 -0
- package/dist/src/ops/registry.js +9 -14
- package/dist/src/plugins/standalone-packager.js +4 -3
- package/dist/src/process/bounded-child-process.d.mts +84 -0
- package/dist/src/process/bounded-child-process.mjs +430 -0
- package/dist/src/process/cli-command.d.mts +16 -0
- package/dist/src/process/cli-command.mjs +75 -0
- package/dist/src/process/signal-child.d.mts +6 -0
- package/dist/src/process/signal-child.mjs +7 -0
- package/dist/src/providers/capability-matrix.js +8 -0
- package/dist/src/providers/capability-matrix.yaml +270 -0
- package/dist/src/providers/deployed-skill-roots.js +20 -0
- package/dist/src/providers/grok-build-acp.js +3 -2
- package/dist/src/providers/grok-build-headless.js +53 -23
- package/dist/src/providers/grok-build-inspect.js +1 -0
- package/dist/src/providers/omp-agent.d.mts +8 -0
- package/dist/src/providers/omp-diagnostics.d.mts +1 -0
- package/dist/src/providers/omp-paths.d.mts +13 -0
- package/dist/src/providers/provider-definitions.js +9 -1
- package/dist/src/providers/provider-definitions.mjs +9 -2
- package/dist/src/providers/skill-ownership.d.mts +9 -0
- package/dist/src/providers/skill-ownership.mjs +88 -0
- package/dist/src/rules/activation.d.mts +21 -0
- package/dist/src/rules/activation.mjs +62 -0
- package/dist/src/rules/inline-budget.d.mts +26 -0
- package/dist/src/rules/inline-budget.mjs +66 -0
- package/dist/src/rules/inventory.js +129 -0
- package/dist/src/rules/pin-state.js +73 -0
- package/dist/src/rules/pins.d.mts +14 -0
- package/dist/src/rules/pins.mjs +112 -0
- package/dist/src/rules/rule-frontmatter.schema.json +197 -0
- package/dist/src/serve/executor-registry.js +16 -24
- package/dist/src/serve/fleet-mission-conductor.js +9 -0
- package/dist/src/serve/mission-conductor.js +47 -12
- package/dist/src/serve/shared-host-scheduler.js +2 -0
- package/dist/src/serve/worker-invocation.js +558 -0
- package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
- package/dist/src/serve/worker-stdin-frame.mjs +107 -0
- package/dist/src/sessions/adapter-registry.js +49 -0
- package/dist/src/sessions/adapters/claude.js +39 -54
- package/dist/src/sessions/adapters/codex.js +82 -67
- package/dist/src/sessions/adapters/copilot.js +4 -4
- package/dist/src/sessions/adapters/cursor.js +40 -57
- package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
- package/dist/src/sessions/adapters/factory.js +31 -27
- package/dist/src/sessions/adapters/generic.js +43 -9
- package/dist/src/sessions/adapters/grok-build.js +11 -11
- package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
- package/dist/src/sessions/adapters/grokbot.js +434 -24
- package/dist/src/sessions/adapters/hermes.js +27 -29
- package/dist/src/sessions/adapters/muse-workspace-scan.js +128 -0
- package/dist/src/sessions/adapters/muse.js +200 -104
- package/dist/src/sessions/adapters/omp.js +28 -14
- package/dist/src/sessions/adapters/openclaw.js +89 -79
- package/dist/src/sessions/adapters/opencode.js +27 -24
- package/dist/src/sessions/adapters/openhuman.js +18 -60
- package/dist/src/sessions/adapters/pi.js +30 -14
- package/dist/src/sessions/adapters/warp.js +3 -3
- package/dist/src/sessions/adapters/windsurf.js +19 -48
- package/dist/src/sessions/batch-contracts.js +25 -5
- package/dist/src/sessions/batch-import.js +72 -51
- package/dist/src/sessions/candidates.js +47 -11
- package/dist/src/sessions/contracts.js +75 -2
- package/dist/src/sessions/coverage-preflight.js +134 -0
- package/dist/src/sessions/discovery.js +3 -0
- package/dist/src/sessions/export-plan.js +4 -3
- package/dist/src/sessions/grok-build-cli.js +67 -5
- package/dist/src/sessions/import-exclusions.js +72 -0
- package/dist/src/sessions/import-lease.js +34 -18
- package/dist/src/sessions/import-limits.js +37 -0
- package/dist/src/sessions/importer.js +84 -46
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/lifecycle.js +45 -0
- package/dist/src/sessions/policy.js +133 -25
- package/dist/src/sessions/readers.js +106 -53
- package/dist/src/sessions/refresh.js +106 -0
- package/dist/src/sessions/repository.js +199 -63
- package/dist/src/sessions/workspace-discovery.js +315 -88
- package/dist/src/sessions/workspace-matching.js +107 -0
- package/dist/src/skills/adapters/clawhub.js +10 -8
- package/dist/src/skills/adapters/openclaw.js +10 -7
- package/dist/src/skills/importer.js +1 -0
- package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
- package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
- package/dist/src/smiths/context-pipeline/finalization.js +6 -5
- package/dist/src/smiths/context-pipeline/generator.js +21 -4
- package/dist/src/smiths/context-pipeline/parallelism-section.js +195 -14
- package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
- package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
- package/dist/src/smiths/mcpsmith/example.js +1 -1
- package/dist/src/smiths/mcpsmith/generator.js +1 -1
- package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +85 -31
- package/dist/src/storage/backends/fs.js +18 -76
- package/dist/src/storage/backends/logseq.js +68 -7
- package/dist/src/storage/backends/obsidian.js +1 -0
- package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
- package/dist/src/testing/generators/test-code-generator.js +57 -30
- package/dist/src/tracker/forge-http.js +2 -1
- package/dist/src/uhp/client.js +11 -1
- package/dist/src/uhp/mission.js +5 -0
- package/dist/src/update/checker.d.mts +2 -0
- package/dist/src/update/checker.mjs +5 -5
- package/dist/src/update/notifier.mjs +54 -2
- package/dist/src/update/service.d.mts +22 -0
- package/dist/src/utils/contained-filesystem.d.mts +37 -0
- package/dist/src/utils/contained-filesystem.mjs +683 -0
- package/dist/src/utils/temporary-directory.js +36 -0
- package/dist/src/writing/example-generator.js +1 -1
- package/dist/src/writing/fidelity.js +18 -1
- package/package.json +32 -2
- package/schemas/mission-v1.schema.json +2429 -0
- package/schemas/models/model-catalog.v1.schema.json +301 -0
- package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
- package/schemas/models/model-policy.v1.schema.json +14 -0
- package/schemas/models/provider-model-capabilities.v1.schema.json +1072 -0
- package/tools/_resolve-impl.mjs +39 -2
- package/tools/agents/deploy-agents.mjs +64 -23
- package/tools/agents/providers/base.mjs +153 -166
- package/tools/agents/providers/claude.mjs +8 -24
- package/tools/agents/providers/codex.mjs +18 -11
- package/tools/agents/providers/cursor.mjs +19 -18
- package/tools/agents/providers/grok-build.mjs +3 -1
- package/tools/agents/providers/grokbot.mjs +11 -12
- package/tools/agents/providers/hermes.mjs +62 -49
- package/tools/agents/providers/omp.mjs +5 -2
- package/tools/agents/providers/openclaw-translator.mjs +19 -5
- package/tools/agents/providers/openclaw.mjs +9 -4
- package/tools/agents/providers/pi.mjs +5 -0
- package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
- package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
- package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
- package/tools/agents/providers/rule-delivery.mjs +1270 -0
- package/tools/agents/providers/warp.mjs +20 -26
- package/tools/cli/decision-offload-report.mjs +40 -0
- package/tools/cli/workspace-status.mjs +862 -0
- package/tools/plugin/package-plugins.mjs +186 -8
- package/tools/plugin/plugin-links.mjs +114 -0
- package/tools/providers/antigravity-transport.mjs +3 -1
- package/tools/providers/deepseek-harness-transport.mjs +360 -0
- package/tools/providers/omp-transport.mjs +194 -0
- package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
- package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
- package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
- package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
- package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
- package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
- package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
- package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
- package/tools/ralph-external/lib/pi-adapter.mjs +329 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +748 -0
- package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
- package/tools/ralph-external/lib/session-environment.mjs +28 -0
- package/tools/ralph-external/lib/systemd-client.mjs +49 -0
- package/tools/ralph-external/lib/worker-health.mjs +439 -0
- package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
- package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
- package/tools/ralph-external/process-monitor.mjs +1017 -0
- package/tools/ralph-external/session-launcher.mjs +5057 -0
- package/tools/scripts/resource-preflight.mjs +541 -0
- package/tools/skills/deploy-skills-codex.mjs +2 -1
- package/vscode-extension/schemas/aiwg.config.v1.json +2916 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { importImpl } from '../_resolve-impl.mjs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { mkdtemp, readFile, rm, writeFile, chmod } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
const { signalChild } = await importImpl(import.meta.url, 'process/signal-child.mjs');
|
|
8
|
+
|
|
9
|
+
export const DSH_SUPPORTED_VERSIONS = Object.freeze(['0.1.1-rc.2', '0.1.2-rc.1', '0.1.3-alpha.1']);
|
|
10
|
+
export const DSH_MAX_FRAME_BYTES = 1024 * 1024;
|
|
11
|
+
export const DSH_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
12
|
+
export const DSH_UPSTREAM_SOURCE = 'https://github.com/deepseek-ai/deepseek-harness';
|
|
13
|
+
export const DSH_UPSTREAM_REVISION = 'd347e703908d0406b7a7ef80e3a0e594d86b2215';
|
|
14
|
+
|
|
15
|
+
const SAFE_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
16
|
+
const SAFE_MODEL = /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,191}$/;
|
|
17
|
+
const SAFE_ENV = /^[A-Z][A-Z0-9_]{0,127}$/;
|
|
18
|
+
const BASE_ENV = ['PATH', 'HOME', 'USER', 'LOGNAME', 'LANG', 'LC_ALL', 'TMPDIR', 'TEMP', 'TMP', 'SystemRoot', 'ComSpec', 'PATHEXT'];
|
|
19
|
+
const SDK_SERVER_NAME = 'deepseek-harness-sdk-runtime';
|
|
20
|
+
const SDK_NOTIFICATION_METHODS = new Set(['session.event', 'session.status', 'subagent.started', 'subagent.finished']);
|
|
21
|
+
|
|
22
|
+
export function assertSupportedDshVersion(version) {
|
|
23
|
+
const normalized = String(version).trim().replace(/^dsh\s+v?/, '').replace(/^v/, '');
|
|
24
|
+
if (!DSH_SUPPORTED_VERSIONS.includes(normalized)) {
|
|
25
|
+
throw new Error(`Unsupported DeepSeek Harness version ${normalized || '(empty)'}; supported: ${DSH_SUPPORTED_VERSIONS.join(', ')}`);
|
|
26
|
+
}
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildCredentialEnvironment({ credentialEnv, credentialValue, dshHome, env = process.env }) {
|
|
31
|
+
if (credentialEnv !== undefined && !SAFE_ENV.test(credentialEnv)) throw new Error('credentialEnv must be an uppercase environment-variable name');
|
|
32
|
+
if ((credentialEnv === undefined) !== (credentialValue === undefined)) throw new Error('credentialEnv and credentialValue must be supplied together');
|
|
33
|
+
if (credentialEnv !== undefined && (typeof credentialValue !== 'string' || credentialValue.length === 0)) throw new Error(`Missing credential value for ${credentialEnv}`);
|
|
34
|
+
if (typeof dshHome !== 'string' || !dshHome) throw new Error('dshHome is required');
|
|
35
|
+
const child = {};
|
|
36
|
+
for (const name of BASE_ENV) if (env[name]) child[name] = env[name];
|
|
37
|
+
if (credentialEnv !== undefined) child[credentialEnv] = credentialValue;
|
|
38
|
+
child.DSH_HOME = resolve(dshHome);
|
|
39
|
+
child.DSH_PERMISSION_MODE = 'workspace-write';
|
|
40
|
+
child.DSH_TELEMETRY_DISABLED = '1';
|
|
41
|
+
return child;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function inspectDshVersion({ binary = 'dsh', cwd = process.cwd(), dshHome, timeoutMs = 10_000 }) {
|
|
45
|
+
const env = buildCredentialEnvironment({ dshHome });
|
|
46
|
+
const result = await runBounded(binary, ['--version'], { cwd: resolve(cwd), env, timeoutMs });
|
|
47
|
+
if (result.code !== 0) throw new Error(`DeepSeek Harness version probe failed (${result.code ?? result.signal})`);
|
|
48
|
+
return assertSupportedDshVersion(result.stdout.trim());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildRoutePatch({ route = 'openrouter', model, credentialEnv = 'OPENROUTER_API_KEY' }) {
|
|
52
|
+
if (!SAFE_ID.test(route)) throw new Error('route must be a lowercase hyphenated identifier');
|
|
53
|
+
if (!SAFE_MODEL.test(model)) throw new Error('model contains unsupported characters');
|
|
54
|
+
if (!SAFE_ENV.test(credentialEnv)) throw new Error('credentialEnv must be an uppercase environment-variable name');
|
|
55
|
+
return [
|
|
56
|
+
'- id: llm-pi-ai',
|
|
57
|
+
' config:',
|
|
58
|
+
' providers:',
|
|
59
|
+
` ${route}:`,
|
|
60
|
+
` apiKeyEnv: ${credentialEnv}`,
|
|
61
|
+
'- id: agent-default-model',
|
|
62
|
+
' config:',
|
|
63
|
+
` provider: ${route}`,
|
|
64
|
+
` model: ${model}`,
|
|
65
|
+
'',
|
|
66
|
+
].join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function createEphemeralRoutePatch(options) {
|
|
70
|
+
const directory = await mkdtemp(join(tmpdir(), 'aiwg-dsh-route-'));
|
|
71
|
+
const path = join(directory, 'route.cordis.patch.yml');
|
|
72
|
+
const content = buildRoutePatch(options);
|
|
73
|
+
await writeFile(path, content, { encoding: 'utf8', mode: 0o600 });
|
|
74
|
+
await chmod(path, 0o600);
|
|
75
|
+
return {
|
|
76
|
+
path,
|
|
77
|
+
hash: `sha256:${createHash('sha256').update(content).digest('hex')}`,
|
|
78
|
+
async cleanup() { await rm(directory, { recursive: true, force: true }); },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function runDshHeadless({ prompt, cwd, dshHome, projectPatch, routePatch, route, model, credentialEnv, credentialValue, binary = 'dsh', timeoutMs = 120_000, terminateGraceMs = 1_000, signal, version }) {
|
|
83
|
+
if (typeof prompt !== 'string' || !prompt.trim()) throw new Error('prompt is required');
|
|
84
|
+
const env = buildCredentialEnvironment({ credentialEnv, credentialValue, dshHome });
|
|
85
|
+
const runtimeVersion = version === undefined
|
|
86
|
+
? await inspectDshVersion({ binary, cwd, dshHome, timeoutMs: Math.min(timeoutMs, 10_000) })
|
|
87
|
+
: assertSupportedDshVersion(version);
|
|
88
|
+
const args = ['--profile', 'headless', '--patch', resolve(projectPatch), '--patch', resolve(routePatch), prompt];
|
|
89
|
+
const result = await runBounded(binary, args, { cwd: resolve(cwd), env, timeoutMs, terminateGraceMs, signal });
|
|
90
|
+
return { ...result, provenance: await provenance({ binary, version: runtimeVersion, profile: 'headless', projectPatch, routePatch, route, model }) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class DshJsonRpcClient {
|
|
94
|
+
constructor({ cwd, dshHome, projectPatch, routePatch, credentialEnv, credentialValue, binary = 'dsh', timeoutMs = 120_000, version }) {
|
|
95
|
+
this.options = { cwd, dshHome, projectPatch, routePatch, credentialEnv, credentialValue, binary, timeoutMs, version };
|
|
96
|
+
this.nextId = 1;
|
|
97
|
+
this.pending = new Map();
|
|
98
|
+
this.notifications = [];
|
|
99
|
+
this.notificationWaiters = new Set();
|
|
100
|
+
this.buffer = '';
|
|
101
|
+
this.outputBytes = 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
start() {
|
|
105
|
+
if (this.child) return;
|
|
106
|
+
const o = this.options;
|
|
107
|
+
const env = buildCredentialEnvironment(o);
|
|
108
|
+
const args = ['--profile', 'sdk', '--patch', resolve(o.projectPatch), '--patch', resolve(o.routePatch)];
|
|
109
|
+
this.child = spawn(o.binary, args, { cwd: resolve(o.cwd), env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
110
|
+
this.stderr = '';
|
|
111
|
+
this.child.stdout.on('data', chunk => this.onData(chunk));
|
|
112
|
+
this.child.stderr.on('data', chunk => {
|
|
113
|
+
try {
|
|
114
|
+
this.accountOutput(chunk);
|
|
115
|
+
this.stderr = appendBounded(this.stderr, chunk, DSH_MAX_OUTPUT_BYTES);
|
|
116
|
+
}
|
|
117
|
+
catch (error) { this.abort(error); }
|
|
118
|
+
});
|
|
119
|
+
this.child.once('error', error => this.rejectAll(error));
|
|
120
|
+
this.child.once('exit', (code, signal) => {
|
|
121
|
+
const suffix = this.buffer.length ? '; truncated JSON-RPC frame remained on stdout' : '';
|
|
122
|
+
this.rejectAll(new Error(`DeepSeek Harness JSON-RPC exited (${code ?? signal})${suffix}`));
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
accountOutput(chunk) {
|
|
127
|
+
this.outputBytes += chunk.length;
|
|
128
|
+
if (this.outputBytes > DSH_MAX_OUTPUT_BYTES) throw new Error('DeepSeek Harness JSON-RPC aggregate output limit exceeded');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
onData(chunk) {
|
|
132
|
+
try { this.accountOutput(chunk); }
|
|
133
|
+
catch (error) { return this.abort(error); }
|
|
134
|
+
this.buffer += chunk.toString('utf8');
|
|
135
|
+
for (;;) {
|
|
136
|
+
const nl = this.buffer.indexOf('\n');
|
|
137
|
+
if (nl < 0) break;
|
|
138
|
+
const line = this.buffer.slice(0, nl).replace(/\r$/, '');
|
|
139
|
+
this.buffer = this.buffer.slice(nl + 1);
|
|
140
|
+
if (Buffer.byteLength(line) > DSH_MAX_FRAME_BYTES) return this.abort(new Error('DeepSeek Harness JSON-RPC frame limit exceeded'));
|
|
141
|
+
if (!line.trim()) continue;
|
|
142
|
+
let message;
|
|
143
|
+
try { message = JSON.parse(line); } catch { return this.abort(new Error('Malformed DeepSeek Harness JSON-RPC frame')); }
|
|
144
|
+
if (message.jsonrpc !== '2.0') return this.abort(new Error('Invalid DeepSeek Harness JSON-RPC version'));
|
|
145
|
+
if (message.id !== undefined) {
|
|
146
|
+
const pending = this.pending.get(message.id);
|
|
147
|
+
if (!pending) continue;
|
|
148
|
+
this.pending.delete(message.id);
|
|
149
|
+
clearTimeout(pending.timer);
|
|
150
|
+
if (message.error) pending.reject(new Error(`DeepSeek Harness JSON-RPC error ${message.error.code}: ${message.error.message}`));
|
|
151
|
+
else pending.resolve(message.result);
|
|
152
|
+
} else if (typeof message.method === 'string') {
|
|
153
|
+
if (!SDK_NOTIFICATION_METHODS.has(message.method)) return this.abort(new Error(`Unsupported DeepSeek Harness JSON-RPC notification: ${message.method}`));
|
|
154
|
+
this.notifications.push(message);
|
|
155
|
+
for (const waiter of [...this.notificationWaiters]) {
|
|
156
|
+
if (waiter.predicate(message)) {
|
|
157
|
+
this.notificationWaiters.delete(waiter);
|
|
158
|
+
clearTimeout(waiter.timer);
|
|
159
|
+
waiter.resolve(message);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} else return this.abort(new Error('Invalid DeepSeek Harness JSON-RPC message shape'));
|
|
163
|
+
}
|
|
164
|
+
if (Buffer.byteLength(this.buffer) > DSH_MAX_FRAME_BYTES) return this.abort(new Error('DeepSeek Harness JSON-RPC frame limit exceeded'));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
request(method, params, { signal, timeoutMs = this.options.timeoutMs } = {}) {
|
|
168
|
+
this.start();
|
|
169
|
+
const id = this.nextId++;
|
|
170
|
+
const frame = JSON.stringify({ jsonrpc: '2.0', id, method, params });
|
|
171
|
+
if (Buffer.byteLength(frame) > DSH_MAX_FRAME_BYTES) return Promise.reject(new Error('DeepSeek Harness JSON-RPC request exceeds frame limit'));
|
|
172
|
+
return new Promise((resolvePromise, reject) => {
|
|
173
|
+
const fail = error => this.abort(error);
|
|
174
|
+
const cancel = () => fail(new Error(`DeepSeek Harness JSON-RPC ${method} cancelled`));
|
|
175
|
+
const timer = setTimeout(() => fail(new Error(`DeepSeek Harness JSON-RPC ${method} timed out`)), timeoutMs);
|
|
176
|
+
this.pending.set(id, {
|
|
177
|
+
resolve: value => { signal?.removeEventListener('abort', cancel); resolvePromise(value); },
|
|
178
|
+
reject: error => { signal?.removeEventListener('abort', cancel); reject(error); },
|
|
179
|
+
timer,
|
|
180
|
+
});
|
|
181
|
+
if (signal?.aborted) return cancel();
|
|
182
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
183
|
+
this.child.stdin.write(`${frame}\n`, error => { if (error) fail(error); });
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async initialize({ provider, model, reasoningEffort, maxTokens }) {
|
|
188
|
+
this.version = this.options.version === undefined
|
|
189
|
+
? await inspectDshVersion({ binary: this.options.binary, cwd: this.options.cwd, dshHome: this.options.dshHome, timeoutMs: Math.min(this.options.timeoutMs, 10_000) })
|
|
190
|
+
: assertSupportedDshVersion(this.options.version);
|
|
191
|
+
const result = await this.request('initialize', { cwd: resolve(this.options.cwd), provider, model, ...(reasoningEffort ? { reasoningEffort } : {}), ...(maxTokens ? { maxTokens } : {}) });
|
|
192
|
+
if (!result || typeof result !== 'object' || result.serverInfo?.name !== SDK_SERVER_NAME || typeof result.serverInfo?.version !== 'string') throw new Error('DeepSeek Harness initialize returned an incompatible server identity');
|
|
193
|
+
this.route = provider;
|
|
194
|
+
this.model = model;
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
async prompt(sessionId, contentBlocks, options) {
|
|
198
|
+
const result = await this.request('session/prompt', { sessionId, contentBlocks }, options);
|
|
199
|
+
if (!result || typeof result !== 'object' || typeof result.messageId !== 'string') throw new Error('DeepSeek Harness session/prompt returned no message id');
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
waitForNotification(predicate, { from = 0, timeoutMs = this.options.timeoutMs } = {}) {
|
|
203
|
+
const existing = this.notifications.slice(from).find(predicate);
|
|
204
|
+
if (existing) return Promise.resolve(existing);
|
|
205
|
+
return new Promise((resolvePromise, reject) => {
|
|
206
|
+
const waiter = { predicate, resolve: resolvePromise, reject, timer: undefined };
|
|
207
|
+
waiter.timer = setTimeout(() => {
|
|
208
|
+
this.notificationWaiters.delete(waiter);
|
|
209
|
+
reject(new Error('DeepSeek Harness notification wait timed out'));
|
|
210
|
+
}, timeoutMs);
|
|
211
|
+
this.notificationWaiters.add(waiter);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
async promptAndWait(sessionId, contentBlocks, { signal, onNotification, timeoutMs = this.options.timeoutMs } = {}) {
|
|
215
|
+
const from = this.notifications.length;
|
|
216
|
+
const result = await this.prompt(sessionId, contentBlocks, { signal, timeoutMs });
|
|
217
|
+
const deadline = Date.now() + timeoutMs;
|
|
218
|
+
let cursor = from;
|
|
219
|
+
let rootRan = false;
|
|
220
|
+
const active = new Set();
|
|
221
|
+
const events = [];
|
|
222
|
+
for (;;) {
|
|
223
|
+
if (signal?.aborted) this.abort(new Error('DeepSeek Harness prompt settlement cancelled'));
|
|
224
|
+
const notification = this.notifications[cursor] ?? await this.waitForNotification(() => true, {
|
|
225
|
+
from: cursor,
|
|
226
|
+
timeoutMs: Math.max(1, deadline - Date.now()),
|
|
227
|
+
});
|
|
228
|
+
cursor = this.notifications.indexOf(notification, cursor) + 1;
|
|
229
|
+
onNotification?.(notification);
|
|
230
|
+
events.push(summarizeNotification(notification));
|
|
231
|
+
const params = notification.params && typeof notification.params === 'object' ? notification.params : {};
|
|
232
|
+
if (notification.method === 'subagent.started' && params.parentSessionId === sessionId && typeof params.childSessionId === 'string') active.add(params.childSessionId);
|
|
233
|
+
if (notification.method === 'subagent.finished' && typeof params.childSessionId === 'string') active.delete(params.childSessionId);
|
|
234
|
+
if (notification.method === 'session.status' && params.sessionId === sessionId) {
|
|
235
|
+
if (params.status === 'running') rootRan = true;
|
|
236
|
+
if (params.status === 'idle' && rootRan && active.size === 0) {
|
|
237
|
+
return { messageId: result.messageId, events, provenance: { provider: 'deepseek-harness', version: this.version, profile: 'sdk', route: this.route, model: this.model } };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
shutdown() { return this.request('shutdown', {}); }
|
|
243
|
+
abort(error = new Error('DeepSeek Harness JSON-RPC client closed')) {
|
|
244
|
+
if (this.child && !this.child.killed) {
|
|
245
|
+
signalChild(this.child, 'SIGTERM');
|
|
246
|
+
const child = this.child;
|
|
247
|
+
const escalation = setTimeout(() => { if (child.exitCode === null) signalChild(child, 'SIGKILL'); }, 1_000);
|
|
248
|
+
child.once('exit', () => clearTimeout(escalation));
|
|
249
|
+
}
|
|
250
|
+
this.rejectAll(error);
|
|
251
|
+
}
|
|
252
|
+
rejectAll(error) {
|
|
253
|
+
for (const pending of this.pending.values()) { clearTimeout(pending.timer); pending.reject(error); }
|
|
254
|
+
this.pending.clear();
|
|
255
|
+
for (const waiter of this.notificationWaiters) { clearTimeout(waiter.timer); waiter.reject(error); }
|
|
256
|
+
this.notificationWaiters.clear();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function provenance({ binary, version, profile, projectPatch, routePatch, route, model }) {
|
|
261
|
+
const [project, routeBytes] = await Promise.all([readFile(projectPatch), readFile(routePatch)]);
|
|
262
|
+
const projectPatchHash = `sha256:${createHash('sha256').update(project).digest('hex')}`;
|
|
263
|
+
const routePatchHash = `sha256:${createHash('sha256').update(routeBytes).digest('hex')}`;
|
|
264
|
+
const profileHash = `sha256:${createHash('sha256').update(JSON.stringify({ profile, projectPatchHash, routePatchHash })).digest('hex')}`;
|
|
265
|
+
return {
|
|
266
|
+
provider: 'deepseek-harness', binary, version, profile, route, model,
|
|
267
|
+
upstreamSource: DSH_UPSTREAM_SOURCE,
|
|
268
|
+
upstreamRevision: DSH_UPSTREAM_REVISION,
|
|
269
|
+
profileHash,
|
|
270
|
+
projectPatchHash,
|
|
271
|
+
routePatchHash,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function summarizeNotification(message) {
|
|
276
|
+
const params = message.params && typeof message.params === 'object' ? message.params : {};
|
|
277
|
+
if (message.method !== 'session.event') return { method: message.method, ...copyScalars(params) };
|
|
278
|
+
const event = params.event && typeof params.event === 'object' ? params.event : {};
|
|
279
|
+
const data = event.data && typeof event.data === 'object' ? event.data : {};
|
|
280
|
+
const summary = { method: message.method, sessionId: params.sessionId, type: event.type, seq: event.seq, time: event.time };
|
|
281
|
+
if (event.type === 'assistant/message') {
|
|
282
|
+
const messageData = data.message && typeof data.message === 'object' ? data.message : {};
|
|
283
|
+
summary.text = textBlocks(messageData.content);
|
|
284
|
+
} else if (String(event.type).startsWith('tool/')) {
|
|
285
|
+
summary.toolName = typeof data.name === 'string' ? data.name : typeof data.toolName === 'string' ? data.toolName : undefined;
|
|
286
|
+
summary.toolCallId = typeof data.id === 'string' ? data.id : typeof data.toolCallId === 'string' ? data.toolCallId : undefined;
|
|
287
|
+
summary.redacted = true;
|
|
288
|
+
} else if (String(event.type).includes('question')) {
|
|
289
|
+
summary.question = typeof data.question === 'string' ? data.question : undefined;
|
|
290
|
+
}
|
|
291
|
+
return summary;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function copyScalars(value) {
|
|
295
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => ['string', 'number', 'boolean'].includes(typeof item)));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function textBlocks(value) {
|
|
299
|
+
if (!Array.isArray(value)) return '';
|
|
300
|
+
return value.flatMap(item => item && typeof item === 'object' && item.type === 'text' && typeof item.text === 'string' ? [item.text] : []).join('\n');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function appendBounded(current, chunk, max) {
|
|
304
|
+
const next = current + chunk.toString('utf8');
|
|
305
|
+
if (Buffer.byteLength(next) > max) throw new Error('DeepSeek Harness output limit exceeded');
|
|
306
|
+
return next;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function runBounded(binary, args, { cwd, env, timeoutMs, terminateGraceMs = 1_000, signal }) {
|
|
310
|
+
return new Promise((resolvePromise, reject) => {
|
|
311
|
+
const child = spawn(binary, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
312
|
+
let stdout = ''; let stderr = ''; let outputBytes = 0; let settled = false; let stopError; let escalation;
|
|
313
|
+
const finish = (error, value) => {
|
|
314
|
+
if (settled) return;
|
|
315
|
+
settled = true;
|
|
316
|
+
clearTimeout(timer);
|
|
317
|
+
clearTimeout(escalation);
|
|
318
|
+
signal?.removeEventListener('abort', cancel);
|
|
319
|
+
error ? reject(error) : resolvePromise(value);
|
|
320
|
+
};
|
|
321
|
+
const terminate = error => {
|
|
322
|
+
if (settled || stopError) return;
|
|
323
|
+
stopError = error;
|
|
324
|
+
// A descendant may retain pipes after the direct child exits. Keep the
|
|
325
|
+
// deadline bounded even when there is no live direct child to terminate.
|
|
326
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
327
|
+
child.stdout.destroy();
|
|
328
|
+
child.stderr.destroy();
|
|
329
|
+
finish(error);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
signalChild(child, 'SIGTERM');
|
|
333
|
+
if (settled) return;
|
|
334
|
+
escalation = setTimeout(() => {
|
|
335
|
+
if (child.exitCode === null && child.signalCode === null) signalChild(child, 'SIGKILL');
|
|
336
|
+
}, terminateGraceMs);
|
|
337
|
+
};
|
|
338
|
+
const cancel = () => terminate(new Error('DeepSeek Harness run cancelled'));
|
|
339
|
+
const timer = setTimeout(() => terminate(new Error('DeepSeek Harness run timed out')), timeoutMs);
|
|
340
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
341
|
+
if (signal?.aborted) cancel();
|
|
342
|
+
child.once('error', error => finish(error));
|
|
343
|
+
const capture = (current, chunk) => {
|
|
344
|
+
outputBytes += chunk.length;
|
|
345
|
+
if (outputBytes > DSH_MAX_OUTPUT_BYTES) throw new Error('DeepSeek Harness aggregate output limit exceeded');
|
|
346
|
+
return appendBounded(current, chunk, DSH_MAX_OUTPUT_BYTES);
|
|
347
|
+
};
|
|
348
|
+
child.stdout.on('data', chunk => { try { stdout = capture(stdout, chunk); } catch (error) { terminate(error); } });
|
|
349
|
+
child.stderr.on('data', chunk => { try { stderr = capture(stderr, chunk); } catch (error) { terminate(error); } });
|
|
350
|
+
child.once('exit', () => {
|
|
351
|
+
if (stopError) {
|
|
352
|
+
child.stdout.destroy();
|
|
353
|
+
child.stderr.destroy();
|
|
354
|
+
finish(stopError);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
// 'exit' can precede the final stdout/stderr data events.
|
|
358
|
+
child.once('close', (code, exitSignal) => finish(stopError, { code, signal: exitSignal, stdout, stderr }));
|
|
359
|
+
});
|
|
360
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { importImpl } from '../_resolve-impl.mjs';
|
|
4
|
+
const { signalChild } = await importImpl(import.meta.url, 'process/signal-child.mjs');
|
|
5
|
+
|
|
6
|
+
const { positiveTimeoutMs } = await importImpl(import.meta.url, 'process/bounded-child-process.mjs');
|
|
7
|
+
|
|
8
|
+
/** Preserve process identity and state: zombies still fail the cleanup gate. */
|
|
9
|
+
export async function inspectOmpProcess({ pid, startTime }) {
|
|
10
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return { pid, present: false };
|
|
11
|
+
try {
|
|
12
|
+
const stat = await readFile(`/proc/${pid}/stat`, 'utf8');
|
|
13
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).trim().split(/\s+/);
|
|
14
|
+
return { pid, state: fields[0], parentPid: Number(fields[1]), processGroup: Number(fields[2]), startTime: fields[19], present: !startTime || startTime === fields[19], identityChanged: Boolean(startTime && startTime !== fields[19]) };
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (process.platform === 'linux' && error.code === 'ENOENT') return { pid, present: false };
|
|
17
|
+
try { process.kill(pid, 0); return { pid, present: true, state: 'unknown' }; }
|
|
18
|
+
catch (probe) { return { pid, present: probe.code !== 'ESRCH', state: probe.code === 'ESRCH' ? undefined : 'unknown' }; }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function waitForOmpProcessCleanup(processes, { timeoutMs = 3000 } = {}) {
|
|
23
|
+
const started = Date.now();
|
|
24
|
+
const initial = await Promise.all(processes.map(inspectOmpProcess));
|
|
25
|
+
let final = initial;
|
|
26
|
+
while (final.some(item => item.present) && Date.now() - started < timeoutMs) {
|
|
27
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(50, Math.max(1, timeoutMs - (Date.now() - started)))));
|
|
28
|
+
final = await Promise.all(processes.map(inspectOmpProcess));
|
|
29
|
+
}
|
|
30
|
+
return { initial, final, elapsedMs: Date.now() - started, complete: final.every(item => !item.present) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
export const MAX_FRAME = 1024 * 1024;
|
|
35
|
+
export const MAX_MESSAGE = 64 * MAX_FRAME;
|
|
36
|
+
function parseFrameJson(text) { try { return JSON.parse(text); } catch { throw new Error('Malformed OMP JSON frame'); } }
|
|
37
|
+
/** Bounded physical JSONL frames and ordered OMP v2 chunk reassembly. */
|
|
38
|
+
export class OmpFrameDecoder {
|
|
39
|
+
constructor({ frameLimit = MAX_FRAME, messageLimit = MAX_MESSAGE } = {}) {
|
|
40
|
+
this.frameLimit = frameLimit; this.messageLimit = messageLimit; this.buffer = Buffer.alloc(0); this.pending = null;
|
|
41
|
+
}
|
|
42
|
+
push(chunk) {
|
|
43
|
+
const frames = [];
|
|
44
|
+
// Split incoming data before buffering to keep individual frame allocations bounded.
|
|
45
|
+
for (const part of Buffer.from(chunk).toString('binary').split('\n').entries()) {
|
|
46
|
+
const [index, value] = part;
|
|
47
|
+
if (index) { if (this.buffer.length) { const frame = this.decode(parseFrameJson(new TextDecoder('utf-8', { fatal: true }).decode(this.buffer))); if (frame) frames.push(frame); } this.buffer = Buffer.alloc(0); }
|
|
48
|
+
const bytes = Buffer.from(value, 'binary');
|
|
49
|
+
if (this.buffer.length + bytes.length + 1 > this.frameLimit) throw new Error('OMP physical frame limit exceeded');
|
|
50
|
+
this.buffer = Buffer.concat([this.buffer, bytes]);
|
|
51
|
+
}
|
|
52
|
+
return frames;
|
|
53
|
+
}
|
|
54
|
+
decode(frame) {
|
|
55
|
+
if (!frame || typeof frame !== 'object' || Array.isArray(frame)) throw new Error('OMP frame must be an object');
|
|
56
|
+
if (frame.type !== 'rpc_chunk') { if (this.pending) throw new Error('OMP chunk sequence interrupted'); return frame; }
|
|
57
|
+
const { chunkId, count, index, byteLength, data } = frame;
|
|
58
|
+
if (typeof chunkId !== 'string' || !chunkId || chunkId.length > 128 || !Number.isSafeInteger(count) || count < 2 || count > 256 || !Number.isSafeInteger(index) || index < 0 || index >= count || !Number.isSafeInteger(byteLength) || byteLength < MAX_FRAME || byteLength > this.messageLimit || typeof data !== 'string' || !data || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(data)) throw new Error('Invalid OMP chunk metadata');
|
|
59
|
+
const bytes = Buffer.from(data, 'base64');
|
|
60
|
+
if (bytes.toString('base64') !== data || bytes.length > 256 * 1024) throw new Error('Invalid OMP chunk payload');
|
|
61
|
+
if (!this.pending) this.pending = { chunkId, count, byteLength, next: 0, size: 0, parts: [] };
|
|
62
|
+
const p = this.pending;
|
|
63
|
+
if (p.chunkId !== chunkId || p.count !== count || p.byteLength !== byteLength || p.next !== index) throw new Error('OMP chunk sequence mismatch');
|
|
64
|
+
p.size += bytes.length; p.next++; p.parts.push(bytes);
|
|
65
|
+
if (p.size > byteLength) throw new Error('OMP chunk declared size exceeded');
|
|
66
|
+
if (p.next < count) return;
|
|
67
|
+
if (p.size !== byteLength) throw new Error('OMP chunk size mismatch');
|
|
68
|
+
this.pending = null;
|
|
69
|
+
const result = parseFrameJson(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(p.parts)));
|
|
70
|
+
if (!result || typeof result !== 'object' || Array.isArray(result) || result.type === 'rpc_chunk') throw new Error('Invalid OMP logical frame');
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
end() { if (this.buffer.length || this.pending) throw new Error('Truncated OMP JSONL output'); }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function summarizeOmpEvents(events, { exitCode = 0 } = {}) {
|
|
77
|
+
let terminal = false, failed = false, aborted = false, assistant = null, elided = false;
|
|
78
|
+
for (const event of events) {
|
|
79
|
+
if (/elided for RPC frame|rpcFrameElidedKeys/.test(JSON.stringify(event))) elided = true;
|
|
80
|
+
if (event.type === 'agent_start' || (event.type === 'agent_end' && event.willContinue === true)) terminal = false;
|
|
81
|
+
if (event.type === 'agent_end' && event.willContinue !== true) terminal = true;
|
|
82
|
+
if (event.type === 'rpc_frame_error' || event.type === 'error' || (event.type === 'tool_execution_end' && event.isError)) failed = true;
|
|
83
|
+
for (const message of [...(event.messages ?? []), ...(event.message ? [event.message] : [])]) {
|
|
84
|
+
if (message.role === 'assistant') assistant = message;
|
|
85
|
+
if (message.stopReason === 'error') failed = true;
|
|
86
|
+
if (message.stopReason === 'aborted') aborted = true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const text = (assistant?.content ?? []).filter(c => c.type === 'text').map(c => c.text).join('\n');
|
|
90
|
+
return { events, settled: terminal, success: terminal && Boolean(assistant) && !failed && !aborted && !elided && exitCode === 0, failed, aborted, elided, text };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Owns one native RPC process. Commands correlate by id; prompt ack is never completion. */
|
|
94
|
+
export class OmpRpcClient {
|
|
95
|
+
// `readyTimeoutMs` bounds only the initial RPC-readiness wait; `timeoutMs` keeps bounding every
|
|
96
|
+
// command and prompt after that, so widening the first-start budget (#2999) never also widens
|
|
97
|
+
// this client's ordinary command/prompt deadlines.
|
|
98
|
+
constructor({ binary = process.env.AIWG_OMP_BIN || 'omp', args = [], cwd, env = process.env, timeoutMs = 30000, readyTimeoutMs = timeoutMs,
|
|
99
|
+
eventLimit = 16 * MAX_FRAME, closeGraceMs = 5000 } = {}) {
|
|
100
|
+
this.closeGraceMs = closeGraceMs; this.timeoutMs = timeoutMs; this.events = []; this.eventBytes = 0; this.eventLimit = eventLimit; this.pending = new Map(); this.counter = 0; this.protocolVersion = 1;
|
|
101
|
+
this.child = spawn(binary, ['--mode', 'rpc', ...args], { cwd, env, stdio: ['pipe', 'pipe', 'pipe'], detached: process.platform !== 'win32' });
|
|
102
|
+
this.decoder = new OmpFrameDecoder(); this.stderr = '';
|
|
103
|
+
this.ready = new Promise((resolve, reject) => { this.readyResolve = resolve; this.readyReject = reject; });
|
|
104
|
+
this.ready.catch(() => {});
|
|
105
|
+
this.readyTimer = setTimeout(() => this.fail(new Error('OMP RPC ready timeout')), readyTimeoutMs);
|
|
106
|
+
this.closed = new Promise(resolve => this.child.once('close', (code) => { this.isClosed = true; try { this.decoder.end(); } catch (e) { this.fail(e); } this.fail(new Error(`OMP RPC exited ${code}`)); resolve(code); }));
|
|
107
|
+
this.child.on('error', e => this.fail(e)); this.child.stdin.on('error', e => this.fail(e));
|
|
108
|
+
this.child.stderr.on('data', b => { this.stderr = (this.stderr + b.toString()).slice(-4096); });
|
|
109
|
+
this.child.stdout.on('data', b => { try { for (const frame of this.decoder.push(b)) this.receive(frame); } catch (e) { this.fail(e); } });
|
|
110
|
+
}
|
|
111
|
+
receive(frame) {
|
|
112
|
+
if (frame.type === 'ready') { clearTimeout(this.readyTimer); this.readyResolve(frame); return; }
|
|
113
|
+
if (frame.type === 'response') {
|
|
114
|
+
const pending = this.pending.get(frame.id);
|
|
115
|
+
if (pending) { this.pending.delete(frame.id); clearTimeout(pending.timer); if (frame.command !== pending.type) pending.reject(new Error('OMP response command mismatch')); else if (!frame.success) pending.reject(new Error(`OMP ${pending.type} failed`)); else pending.resolve(frame.data); }
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const size = Buffer.byteLength(JSON.stringify(frame));
|
|
119
|
+
if ((this.eventBytes += size) > this.eventLimit) throw new Error('OMP event retention limit exceeded');
|
|
120
|
+
this.events.push(frame); this.onEvent?.(frame);
|
|
121
|
+
}
|
|
122
|
+
async connect() { const ready = await this.ready; if (ready.supportedProtocolVersions?.includes(2)) { await this.command('negotiate_protocol', { protocolVersion: 2 }); this.protocolVersion = 2; } return this; }
|
|
123
|
+
async command(type, data = {}) {
|
|
124
|
+
if (this.failure || this.isClosed) throw this.failure ?? new Error('OMP closed');
|
|
125
|
+
const id = `aiwg-${++this.counter}`;
|
|
126
|
+
const line = JSON.stringify({ ...data, type, id }) + '\n';
|
|
127
|
+
if (Buffer.byteLength(line) > MAX_FRAME) throw new Error('OMP command frame limit exceeded');
|
|
128
|
+
const response = new Promise((resolve, reject) => { const timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`OMP ${type} timeout`)); void this.close(); }, this.timeoutMs); this.pending.set(id, { resolve, reject, timer, type }); });
|
|
129
|
+
// Awaiting write's callback applies backpressure and catches pipe failure.
|
|
130
|
+
await new Promise((resolve, reject) => this.child.stdin.write(line, error => error ? reject(error) : resolve())).catch(error => this.fail(error));
|
|
131
|
+
return response;
|
|
132
|
+
}
|
|
133
|
+
async prompt(message, { signal } = {}) {
|
|
134
|
+
if (this.running) throw new Error('OMP client permits one in-flight prompt');
|
|
135
|
+
this.running = true; this.events = []; this.eventBytes = 0;
|
|
136
|
+
try {
|
|
137
|
+
const terminal = new Promise((resolve, reject) => {
|
|
138
|
+
this.promptReject = reject;
|
|
139
|
+
this.onEvent = frame => { if (frame.type === 'agent_end' && frame.willContinue !== true) resolve(); };
|
|
140
|
+
});
|
|
141
|
+
terminal.catch(() => {});
|
|
142
|
+
const abort = () => { this.promptReject?.(new Error('OMP prompt aborted')); void this.close(); };
|
|
143
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
144
|
+
const timer = setTimeout(abort, this.timeoutMs);
|
|
145
|
+
try { if (signal?.aborted) abort(); await this.command('prompt', { message }); await terminal; const state = await this.command('get_state'); if (state?.isStreaming || state?.queuedMessageCount > 0 || state?.isCompacting) throw new Error('OMP terminal event preceded pending work'); return summarizeOmpEvents(this.events); }
|
|
146
|
+
finally { clearTimeout(timer); signal?.removeEventListener('abort', abort); this.onEvent = null; this.promptReject = null; }
|
|
147
|
+
} catch (error) { await this.close(); throw error; } finally { this.running = false; }
|
|
148
|
+
}
|
|
149
|
+
fail(error) { if (this.failure) return; this.failure = error; clearTimeout(this.readyTimer); this.readyReject(error); this.promptReject?.(error); for (const p of this.pending.values()) { clearTimeout(p.timer); p.reject(error); } this.pending.clear(); if (!this.isClosed) this.kill('SIGKILL'); }
|
|
150
|
+
kill(signal) { try { if (process.platform !== 'win32' && Number.isSafeInteger(this.child.pid) && this.child.pid > 0) process.kill(-this.child.pid, signal); else signalChild(this.child, signal); } catch { /* already exited */ } }
|
|
151
|
+
async close() {
|
|
152
|
+
if (this.isClosed) return this.closed;
|
|
153
|
+
if (this.closing) return this.closing;
|
|
154
|
+
this.closing = (async () => {
|
|
155
|
+
// Native EOF drains accepted commands and disposes sessions/MCP clients.
|
|
156
|
+
// Signalling the whole group first can orphan children before OMP reaps them.
|
|
157
|
+
if (!this.child.stdin.destroyed && !this.child.stdin.writableEnded)
|
|
158
|
+
this.child.stdin.end(JSON.stringify({ type: 'abort', id: 'aiwg-close' }) + '\n');
|
|
159
|
+
let killTimer;
|
|
160
|
+
const termTimer = setTimeout(() => {
|
|
161
|
+
this.kill('SIGTERM');
|
|
162
|
+
killTimer = setTimeout(() => this.kill('SIGKILL'), 500);
|
|
163
|
+
}, this.closeGraceMs);
|
|
164
|
+
try { return await this.closed; }
|
|
165
|
+
finally { clearTimeout(termTimer); clearTimeout(killTimer); this.kill('SIGKILL'); }
|
|
166
|
+
})();
|
|
167
|
+
return this.closing;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* #2999: a pinned native binary's first cold start competes with whatever else the host is
|
|
173
|
+
* running (e.g. another CI job) for CPU and page cache, so a fixed RPC-readiness wait can fail
|
|
174
|
+
* even though the binary is healthy. `warmArgs` touches the binary once, strictly before the RPC
|
|
175
|
+
* process starts, outside the readiness window (default `--version`, matching the warm-up
|
|
176
|
+
* `omp-conformance.mjs` already performs) so most of that cold-start cost lands on `warmedMs`
|
|
177
|
+
* instead of the RPC `readyMs` budget. Callers pass a larger `readyTimeoutMs` (never `timeoutMs`,
|
|
178
|
+
* which would also widen every later command/prompt deadline on the returned client) for the
|
|
179
|
+
* first-start budget. All three durations/outcomes are returned so a caller can record them: a
|
|
180
|
+
* slow start then shows up as a growing number in a conformance report, not only as a timeout.
|
|
181
|
+
*/
|
|
182
|
+
export async function warmAndConnectOmpRpcClient({ binary, cwd, env, warmArgs = ['--version'], warmTimeoutMs = 15000, ...clientOptions }) {
|
|
183
|
+
const warmStarted = Date.now();
|
|
184
|
+
// warmTimeoutMs is caller-controlled; clamp it so a caller passing 0 (or
|
|
185
|
+
// anything else non-positive) can't disable the bound (#3062 B/C finding 8).
|
|
186
|
+
const warmup = spawnSync(binary, warmArgs, { cwd, env, encoding: 'utf8', timeout: positiveTimeoutMs(warmTimeoutMs, 15000), killSignal: 'SIGKILL' });
|
|
187
|
+
const warmedMs = Date.now() - warmStarted;
|
|
188
|
+
const warmupResult = { status: warmup.status ?? null, signal: warmup.signal ?? null, error: warmup.error ? String(warmup.error.message ?? warmup.error) : null };
|
|
189
|
+
const readyStarted = Date.now();
|
|
190
|
+
const client = new OmpRpcClient({ binary, cwd, env, ...clientOptions });
|
|
191
|
+
try { await client.connect(); }
|
|
192
|
+
catch (error) { await client.close(); throw error; }
|
|
193
|
+
return { client, warmup: warmupResult, warmedMs, readyMs: Date.now() - readyStarted };
|
|
194
|
+
}
|