@moondeck/station 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -2
- package/bin/uai-host.mjs +130 -0
- package/db/migrations/0000_host_tasks.sql +12 -0
- package/db/migrations/0001_host_ui.sql +11 -0
- package/db/migrations/0002_host_github_tokens.sql +8 -0
- package/db/migrations/0003_host_ssh_keys.sql +8 -0
- package/db/migrations/0004_host_owner_name.sql +1 -0
- package/db/migrations/0005_host_env_vars.sql +5 -0
- package/db/migrations/0006_host_project_env.sql +9 -0
- package/db/migrations/0007_host_github_token_kind.sql +7 -0
- package/db/migrations/0008_host_mcp_connections.sql +18 -0
- package/db/migrations/0009_host_agent_sessions.sql +15 -0
- package/db/migrations/0010_host_engine_accounts.sql +13 -0
- package/db/migrations/0011_codex_thread_id.sql +5 -0
- package/db/migrations/0012_agent_session_attach_key.sql +4 -0
- package/db/migrations/0013_github_credential_generations.sql +11 -0
- package/db/migrations/0014_host_inventory_event_index.sql +1 -0
- package/db/migrations/0015_host_settings.sql +9 -0
- package/db/migrations/0016_task_environment.sql +2 -0
- package/db/migrations/0017_engine_account_scope.sql +7 -0
- package/db/migrations/0018_engine_account_position.sql +3 -0
- package/db/migrations/0019_engine_account_store_ref.sql +3 -0
- package/db/migrations/0020_user_credential_store_ref.sql +6 -0
- package/db/migrations/0021_project_env_store_ref.sql +3 -0
- package/db/migrations/0022_task_project_ids.sql +5 -0
- package/db/migrations/0023_project_env_org.sql +4 -0
- package/db/migrations/0024_task_env_mode.sql +9 -0
- package/db/migrations/0025_project_env_authority.sql +147 -0
- package/db/migrations/0026_project_env_replica.sql +150 -0
- package/db/migrations/0027_cloud_credential_foundation.sql +51 -0
- package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
- package/db/migrations/0029_task_shared_files_mode.sql +5 -0
- package/db/migrations/0030_credential_cleanup_restops.sql +12 -0
- package/db/migrations/meta/_journal.json +223 -0
- package/db/schema.ts +850 -0
- package/images/standard/Dockerfile +347 -0
- package/images/standard/README.md +167 -0
- package/images/standard/container/code-server-settings.json +63 -0
- package/images/standard/container/corepack-version +1 -0
- package/images/standard/container/uai-init +704 -0
- package/images/standard/container/uai-materialize-runtimes +1527 -0
- package/images/standard/container/uai-task-forwarder.mjs +303 -0
- package/images/standard/tool-versions +2 -0
- package/lib/__fixtures__/skill-index/SKILL.md +10 -0
- package/lib/__fixtures__/skill-index/skills/accessibility-checklist.md +7 -0
- package/lib/__fixtures__/skill-index/skills/brand-voice.md +9 -0
- package/lib/__fixtures__/skill-index/skills/design-tokens.md +9 -0
- package/lib/agent-cli.ts +1507 -0
- package/lib/agent-errors.ts +22 -0
- package/lib/agent.ts +672 -0
- package/lib/agents/attach-compatibility.ts +51 -0
- package/lib/agents/claude.ts +918 -0
- package/lib/agents/codex-token-usage.fixture.json +130 -0
- package/lib/agents/codex.ts +1392 -0
- package/lib/agents/cursor-period-usage.ts +253 -0
- package/lib/agents/cursor.ts +562 -0
- package/lib/agents/dispatch.ts +78 -0
- package/lib/agents/durable-proc.ts +334 -0
- package/lib/agents/factory.ts +57 -0
- package/lib/agents/grok.ts +381 -0
- package/lib/agents/kimi.ts +329 -0
- package/lib/agents/machine-durable.ts +604 -0
- package/lib/agents/mock.ts +133 -0
- package/lib/agents/mode.ts +64 -0
- package/lib/agents/opencode.ts +508 -0
- package/lib/agents/output-source.ts +3 -0
- package/lib/agents/proc.ts +221 -0
- package/lib/agents/rate-limit.ts +337 -0
- package/lib/agents/registry.ts +182 -0
- package/lib/agents/session-state.ts +197 -0
- package/lib/agents/skill-index-preamble.fixture.ts +26 -0
- package/lib/agents/transport.ts +741 -0
- package/lib/agents/types.ts +254 -0
- package/lib/agents/uai.ts +106 -0
- package/lib/agents/usage.ts +44 -0
- package/lib/apple-one-off.ts +277 -0
- package/lib/apple-runtime-recycle.ts +237 -0
- package/lib/apple-uninstall-teardown.ts +224 -0
- package/lib/assignment-evidence.ts +94 -0
- package/lib/attachments.ts +48 -0
- package/lib/browser-testing.ts +1537 -0
- package/lib/capture-flow.ts +332 -0
- package/lib/cloud-credential-cleanup-adapter.ts +123 -0
- package/lib/cloud-credential-host.ts +512 -0
- package/lib/cloud-credential-leases.ts +2587 -0
- package/lib/cloud-credential-runtime.ts +1357 -0
- package/lib/cloud-state.ts +56 -0
- package/lib/codex-auth.ts +456 -0
- package/lib/command-db.ts +310 -0
- package/lib/compose-network-recovery.ts +283 -0
- package/lib/connection-secret-redaction.ts +25 -0
- package/lib/container-runtime.ts +1453 -0
- package/lib/credential-cleanup-inventory.ts +490 -0
- package/lib/credential-consumers/engine-anchor.ts +1880 -0
- package/lib/credential-consumers/engines.ts +797 -0
- package/lib/credential-consumers/files.ts +934 -0
- package/lib/credential-consumers/github.ts +702 -0
- package/lib/credential-consumers/mcp.ts +389 -0
- package/lib/credential-consumers/task-files.ts +127 -0
- package/lib/credential-consumers/task-secrets.ts +10 -0
- package/lib/credential-store-pickup.ts +149 -0
- package/lib/credential-store-scope.ts +112 -0
- package/lib/credential-store.ts +932 -0
- package/lib/credentials-activation-authority.ts +709 -0
- package/lib/credentials-purge.ts +1006 -0
- package/lib/custom-connection-capture.ts +395 -0
- package/lib/custom-connection-launch.ts +384 -0
- package/lib/custom-connection-runtime.ts +141 -0
- package/lib/db.ts +80 -0
- package/lib/disk-pressure.ts +42 -0
- package/lib/docker-exec.ts +156 -0
- package/lib/engine-accounts.ts +1970 -0
- package/lib/engine-connect.ts +321 -0
- package/lib/engine-credential-env.ts +52 -0
- package/lib/engine-login.ts +2933 -0
- package/lib/engines.ts +1342 -0
- package/lib/enrollment-state.ts +251 -0
- package/lib/env-file.ts +246 -0
- package/lib/env-schema.ts +891 -0
- package/lib/env.ts +195 -0
- package/lib/fs-aliases.ts +49 -0
- package/lib/git-clone-credentials.ts +334 -0
- package/lib/git-diff.ts +530 -0
- package/lib/git-identity.ts +715 -0
- package/lib/github-git-auth.ts +207 -0
- package/lib/github-origin.ts +28 -0
- package/lib/github-tokens.ts +2439 -0
- package/lib/host-cloud-url.ts +62 -0
- package/lib/host-config.ts +279 -0
- package/lib/host-env.ts +424 -0
- package/lib/host-logs.ts +965 -0
- package/lib/internal-connections.ts +5 -0
- package/lib/keyed-promise-tail.ts +23 -0
- package/lib/launch-mode.ts +25 -0
- package/lib/lease.ts +239 -0
- package/lib/legacy-host.ts +769 -0
- package/lib/legacy-runtime-retirement.ts +424 -0
- package/lib/legacy-runtime-v1.fixture.ts +627 -0
- package/lib/machine-exec.ts +238 -0
- package/lib/machine-gateway-tunnel.ts +344 -0
- package/lib/machine-keys.ts +65 -0
- package/lib/machine-project-env-loader.ts +152 -0
- package/lib/machine-provider-aws.ts +923 -0
- package/lib/machine-provider-local.ts +338 -0
- package/lib/machine-provider.ts +90 -0
- package/lib/managed-activation-watcher.ts +72 -0
- package/lib/managed-install-owner-watcher.ts +55 -0
- package/lib/managed-operation-drain.ts +57 -0
- package/lib/managed-runtime.ts +4296 -0
- package/lib/managed-update-scheduler.ts +125 -0
- package/lib/mcp-capture.ts +865 -0
- package/lib/mcp-config-lock.ts +2 -0
- package/lib/mcp-connections.ts +959 -0
- package/lib/mcp-gateway.ts +2594 -0
- package/lib/obs.ts +512 -0
- package/lib/openrouter-budget.ts +247 -0
- package/lib/orchestrator.ts +11409 -0
- package/lib/person-capture.ts +327 -0
- package/lib/preview-ports.ts +85 -0
- package/lib/preview-sidecar.ts +331 -0
- package/lib/project-env-authority.ts +1319 -0
- package/lib/project-env-feature.ts +74 -0
- package/lib/project-env-legacy-tags.ts +130 -0
- package/lib/project-env-loader.ts +242 -0
- package/lib/project-env-replica.ts +2094 -0
- package/lib/project-env-schema-capture.ts +342 -0
- package/lib/project-env-staging.ts +148 -0
- package/lib/project-env-task-policy.ts +176 -0
- package/lib/project-env-task-replica.ts +1752 -0
- package/lib/release-feed.ts +61 -0
- package/lib/release-manifest.ts +710 -0
- package/lib/release-trust.ts +46 -0
- package/lib/repo-clone.ts +38 -0
- package/lib/routing/shadow.ts +261 -0
- package/lib/runner-boot.ts +10 -0
- package/lib/runtime-activation-tail.ts +279 -0
- package/lib/runtime-archive.ts +1216 -0
- package/lib/runtime-authority.ts +166 -0
- package/lib/runtime-guard.ts +36 -0
- package/lib/runtime-provider-state.ts +169 -0
- package/lib/runtime-reconnect.ts +86 -0
- package/lib/runtime-state.ts +1033 -0
- package/lib/safe-http.ts +364 -0
- package/lib/secrets-proxy/address.ts +95 -0
- package/lib/secrets-proxy/audit.ts +30 -0
- package/lib/secrets-proxy/ca-store.ts +200 -0
- package/lib/secrets-proxy/ca.ts +470 -0
- package/lib/secrets-proxy/container-steps.ts +138 -0
- package/lib/secrets-proxy/der.ts +273 -0
- package/lib/secrets-proxy/engine.ts +1633 -0
- package/lib/secrets-proxy/host.ts +93 -0
- package/lib/secrets-proxy/hostname.ts +57 -0
- package/lib/secrets-proxy/placeholders.ts +196 -0
- package/lib/secrets-proxy/policy.ts +150 -0
- package/lib/secrets-proxy/readiness.ts +148 -0
- package/lib/secrets-proxy/runtime.ts +582 -0
- package/lib/secrets-proxy/scrub.ts +559 -0
- package/lib/secrets-proxy/sidecar-view.ts +139 -0
- package/lib/secrets-proxy/task-scripts.ts +262 -0
- package/lib/secrets-proxy/task-trust.ts +605 -0
- package/lib/secrets-proxy/trust-watch.ts +80 -0
- package/lib/secrets.ts +71 -0
- package/lib/shared-files-s3.ts +976 -0
- package/lib/shared-files.ts +156 -0
- package/lib/sidecar-assign.ts +49 -0
- package/lib/sidecar-guard.ts +58 -0
- package/lib/sidecar-resume.ts +178 -0
- package/lib/sidecar.ts +69 -0
- package/lib/signin-box-credentials.ts +465 -0
- package/lib/signin-box.ts +114 -0
- package/lib/skill-index-example.fixture.ts +56 -0
- package/lib/skill-layout.ts +417 -0
- package/lib/skills.ts +489 -0
- package/lib/ssh.ts +401 -0
- package/lib/standard-image-cleanup.ts +117 -0
- package/lib/standard-image-pin.ts +207 -0
- package/lib/standard-image.ts +1697 -0
- package/lib/stopped-task-status-queue.ts +120 -0
- package/lib/task-container-cli.ts +269 -0
- package/lib/task-credential-cleanup.ts +752 -0
- package/lib/task-diff-sandbox.ts +464 -0
- package/lib/task-diff.ts +91 -0
- package/lib/task-env-forwarder.ts +228 -0
- package/lib/task-env-source.ts +249 -0
- package/lib/task-environment/anchor.ts +31 -0
- package/lib/task-environment/apple-container.ts +860 -0
- package/lib/task-environment/docker.ts +1344 -0
- package/lib/task-environment/index.ts +466 -0
- package/lib/task-environment/legacy-adoption.ts +467 -0
- package/lib/task-environment/machine-environment.ts +61 -0
- package/lib/task-environment/machine-task-up.ts +1227 -0
- package/lib/task-environment/machine.ts +974 -0
- package/lib/task-environment/registry.ts +59 -0
- package/lib/task-environment/self-identity.ts +20 -0
- package/lib/task-environment/self.ts +566 -0
- package/lib/task-environment/types.ts +642 -0
- package/lib/task-environment/workspace-files.ts +685 -0
- package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
- package/lib/task-files-machine-retirement-outbox.ts +592 -0
- package/lib/task-files-machine-retirement.ts +116 -0
- package/lib/task-files-readiness.ts +127 -0
- package/lib/task-gateway-port.ts +22 -0
- package/lib/task-gateway-protocol.ts +190 -0
- package/lib/task-gateway-recovery.ts +47 -0
- package/lib/task-gateway-state.ts +512 -0
- package/lib/task-identity.ts +19 -0
- package/lib/task-inventory.ts +682 -0
- package/lib/task-machine-credentials-activation.ts +95 -0
- package/lib/task-machine-swap-safety.ts +729 -0
- package/lib/task-person-credentials.ts +76 -0
- package/lib/task-project-ops-host.ts +147 -0
- package/lib/task-project-ops.ts +1062 -0
- package/lib/task-secret-staging.ts +16 -0
- package/lib/task-status.ts +46 -0
- package/lib/task-token.ts +46 -0
- package/lib/token-broker-adopt.ts +344 -0
- package/lib/token-broker-lambda.ts +105 -0
- package/lib/token-broker-local.ts +392 -0
- package/lib/token-broker-revision.ts +310 -0
- package/lib/token-broker-ssm.ts +525 -0
- package/lib/token-broker.ts +218 -0
- package/lib/token-lanes/openai-oauth.ts +123 -0
- package/lib/token-lanes/registry.ts +45 -0
- package/lib/token-lanes/types.ts +163 -0
- package/lib/token-lanes/xai-oauth.ts +165 -0
- package/lib/transcript.ts +128 -0
- package/lib/tunnel-registry.ts +220 -0
- package/lib/tunnel-runtime.ts +235 -0
- package/lib/ulid.ts +16 -0
- package/lib/user-bus-env.ts +116 -0
- package/lib/user-credentials-store.ts +435 -0
- package/lib/vnode-pressure.ts +142 -0
- package/package.json +131 -4
- package/runner/runner.mjs +442 -0
- package/scripts/agent/_common.sh +633 -0
- package/scripts/agent/env-hook.sh +626 -0
- package/scripts/agent/moondeck-cli.sh +5 -0
- package/scripts/agent/task-down.sh +291 -0
- package/scripts/agent/task-status.sh +70 -0
- package/scripts/agent/task-up.sh +3761 -0
- package/scripts/agent/uai-env-exec.sh +26 -0
- package/scripts/install/darwin.ts +1048 -0
- package/scripts/install/linux.ts +958 -0
- package/scripts/install/types.ts +78 -0
- package/scripts/install/util.ts +254 -0
- package/scripts/install/win.ts +143 -0
- package/src/activation-respawn.ts +155 -0
- package/src/apple-tunnel-route.ts +104 -0
- package/src/capture-wire.ts +102 -0
- package/src/cli.ts +2583 -0
- package/src/cloud-credentials.ts +2144 -0
- package/src/cloud-origins.ts +23 -0
- package/src/credential-location.ts +248 -0
- package/src/credential-wire-validation.ts +25 -0
- package/src/custom-connections.ts +237 -0
- package/src/engine-account-paths.ts +11 -0
- package/src/engine-models.ts +39 -0
- package/src/engine-usage.ts +104 -0
- package/src/event-outbox.ts +185 -0
- package/src/index.ts +2899 -0
- package/src/load-env.ts +97 -0
- package/src/machine-tunnel-route.ts +156 -0
- package/src/main.ts +6349 -0
- package/src/never-proxied-domains.ts +31 -0
- package/src/outbound-frame.ts +31 -0
- package/src/paths.ts +81 -0
- package/src/project-env-latest.ts +130 -0
- package/src/project-env-policy.ts +396 -0
- package/src/project-env-wording.ts +53 -0
- package/src/protocol.ts +5545 -0
- package/src/runtime-bootstrap.ts +165 -0
- package/src/station-identity.ts +96 -0
- package/src/task-env-names.ts +28 -0
- package/src/task-secrets.ts +75 -0
- package/src/tunnel-dial.ts +102 -0
- package/src/ui/server.ts +852 -0
- package/src/ui/types.ts +173 -0
- package/src/workspace-path-policy.ts +41 -0
- package/ui/app.js +1102 -0
- package/ui/index.html +90 -0
- package/ui/style.css +685 -0
- package/ui/uai-favicon.svg +21 -0
- package/ui/uai-logo-black.svg +9 -0
- package/ui/uai-wheel.svg +9 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the secret-free account usage report sent to Uai Cloud.
|
|
3
|
+
*
|
|
4
|
+
* A leased env account carries both a variable name and its secret value in
|
|
5
|
+
* runner memory. Billing is deliberately inferred from the NAME alone. Some
|
|
6
|
+
* file-backed accounts use the same `auth.json` name for a subscription and
|
|
7
|
+
* an API key, so they carry no hint.
|
|
8
|
+
*
|
|
9
|
+
* The hint names the revision it was read from, and is given only when that is
|
|
10
|
+
* the revision the reporting session runs on: after a re-paste, a lease
|
|
11
|
+
* refresh can hand the runner newer material than an agent is still using.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { EngineUsageFrame, EngineUsageWindow } from "./protocol";
|
|
15
|
+
|
|
16
|
+
type EngineUsageBilling = NonNullable<EngineUsageFrame["billing"]>;
|
|
17
|
+
|
|
18
|
+
export interface LeasedBillingAccount {
|
|
19
|
+
credentialId: string;
|
|
20
|
+
revision: string;
|
|
21
|
+
engineKind: string;
|
|
22
|
+
authKind: "env" | "config-dir";
|
|
23
|
+
env: Readonly<Record<string, string>> | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface EngineUsageObservation {
|
|
27
|
+
status: EngineUsageFrame["status"];
|
|
28
|
+
resetsAt?: number;
|
|
29
|
+
usingOverage?: boolean;
|
|
30
|
+
windows: EngineUsageWindow[];
|
|
31
|
+
source: EngineUsageFrame["source"];
|
|
32
|
+
observedAt: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const BILLING_BY_VARIABLE: Readonly<
|
|
36
|
+
Record<string, Readonly<Record<string, EngineUsageBilling>>>
|
|
37
|
+
> = Object.freeze({
|
|
38
|
+
claude: Object.freeze({
|
|
39
|
+
// Capture files every pasted Claude secret that is NOT `sk-ant-api…` under
|
|
40
|
+
// this name (engine-accounts.ts `pastedSecretMaterial`), so the name alone
|
|
41
|
+
// also covers a token whose prefix told capture nothing, which the cloud
|
|
42
|
+
// left unknown. Subscription is still right for a report: usage is read
|
|
43
|
+
// from a session that worked, and Claude Code authenticates this variable
|
|
44
|
+
// only as an OAuth (subscription) token, so a session that ran on it was
|
|
45
|
+
// one. Do not "correct" it to unknown.
|
|
46
|
+
CLAUDE_CODE_OAUTH_TOKEN: "subscription",
|
|
47
|
+
ANTHROPIC_API_KEY: "api-key",
|
|
48
|
+
}),
|
|
49
|
+
cursor: Object.freeze({ CURSOR_API_KEY: "api-key" }),
|
|
50
|
+
grok: Object.freeze({ XAI_API_KEY: "api-key" }),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/** The billing lane when one leased variable name tells it unambiguously. */
|
|
54
|
+
export function billingFromLeasedVariableName(
|
|
55
|
+
account: LeasedBillingAccount | null | undefined,
|
|
56
|
+
): EngineUsageBilling | undefined {
|
|
57
|
+
if (!account || account.authKind !== "env" || account.env === null) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const names = Object.keys(account.env);
|
|
61
|
+
if (names.length !== 1) return undefined;
|
|
62
|
+
return BILLING_BY_VARIABLE[account.engineKind]?.[names[0]!];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Construct the exact wire frame for one adapter usage observation.
|
|
66
|
+
* `revision` is the one the reporting session runs on; a leased account at
|
|
67
|
+
* any other revision gives no hint. */
|
|
68
|
+
export function buildEngineUsageFrame(input: {
|
|
69
|
+
reportId: string;
|
|
70
|
+
taskId: string;
|
|
71
|
+
agentId: string;
|
|
72
|
+
engine: EngineUsageFrame["engine"];
|
|
73
|
+
credentialId: string;
|
|
74
|
+
revision?: string;
|
|
75
|
+
usage: EngineUsageObservation;
|
|
76
|
+
leasedAccount?: LeasedBillingAccount | null;
|
|
77
|
+
}): EngineUsageFrame {
|
|
78
|
+
const { usage } = input;
|
|
79
|
+
const account =
|
|
80
|
+
input.revision !== undefined &&
|
|
81
|
+
input.leasedAccount?.credentialId === input.credentialId &&
|
|
82
|
+
input.leasedAccount.revision === input.revision &&
|
|
83
|
+
input.leasedAccount.engineKind === input.engine
|
|
84
|
+
? input.leasedAccount
|
|
85
|
+
: null;
|
|
86
|
+
const billing = billingFromLeasedVariableName(account);
|
|
87
|
+
return {
|
|
88
|
+
kind: "engine.usage",
|
|
89
|
+
reportId: input.reportId,
|
|
90
|
+
taskId: input.taskId,
|
|
91
|
+
agentId: input.agentId,
|
|
92
|
+
engine: input.engine,
|
|
93
|
+
credentialId: input.credentialId,
|
|
94
|
+
...(billing === undefined ? {} : { billing, revision: account!.revision }),
|
|
95
|
+
status: usage.status,
|
|
96
|
+
...(usage.resetsAt === undefined ? {} : { resetsAt: usage.resetsAt }),
|
|
97
|
+
...(usage.usingOverage === undefined
|
|
98
|
+
? {}
|
|
99
|
+
: { usingOverage: usage.usingOverage }),
|
|
100
|
+
windows: usage.windows,
|
|
101
|
+
source: usage.source,
|
|
102
|
+
observedAt: usage.observedAt,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-103: the host-side event outbox.
|
|
3
|
+
*
|
|
4
|
+
* Every HostEvent gets a monotonic seq and its serialized wire frame is held
|
|
5
|
+
* here until the cloud acks it. The bridge connection drains the outbox in
|
|
6
|
+
* order — live traffic and reconnect replay are the same code path, so a
|
|
7
|
+
* WSS blip (a cloud deploy) can no longer drop events on the floor.
|
|
8
|
+
*
|
|
9
|
+
* Bounded, in-memory. A host PROCESS restart still loses unsent events —
|
|
10
|
+
* that path is ADR-061's (the restarted host reattaches to the container
|
|
11
|
+
* and resumes streaming); this outbox closes the disconnect gap only.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { MAX_OUTBOUND_FRAME_BYTES } from "./outbound-frame";
|
|
15
|
+
import type { HostEvent } from "./protocol";
|
|
16
|
+
|
|
17
|
+
export interface OutboxEntry {
|
|
18
|
+
seq: number;
|
|
19
|
+
/** The full serialized `{kind:"event", seq, event}` frame, ready to send. */
|
|
20
|
+
raw: string;
|
|
21
|
+
/** Exact UTF-8 wire bytes retained by this frame. */
|
|
22
|
+
bytes: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface EventOutboxSink {
|
|
26
|
+
readonly bufferedAmount: number;
|
|
27
|
+
send(raw: string): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type EventOutboxTransmitResult =
|
|
31
|
+
| "empty"
|
|
32
|
+
| "backpressured"
|
|
33
|
+
| "send_failed";
|
|
34
|
+
|
|
35
|
+
/** Overflow bounds. Generous: a disconnect longer than this many events is a
|
|
36
|
+
* real outage, not a deploy blip, and dropping OLDEST keeps the tail — the
|
|
37
|
+
* most recent turn — intact for replay. */
|
|
38
|
+
const MAX_ENTRIES = 10_000;
|
|
39
|
+
const MAX_BYTES = 32 * 1024 * 1024;
|
|
40
|
+
// The one ceiling on anything the runner sends (src/outbound-frame.ts), under
|
|
41
|
+
// the authenticated cloud receiver's. Task diffs and message_complete events
|
|
42
|
+
// are not yet field-bounded, so rolling hosts must retain one legitimate
|
|
43
|
+
// frame above the ordinary outbox/transport budgets. Anything larger is
|
|
44
|
+
// refused here, counted as dropped: it would never reach the cloud, and could
|
|
45
|
+
// otherwise put the host into a permanent reconnect loop.
|
|
46
|
+
export const MAX_EVENT_FRAME_BYTES = MAX_OUTBOUND_FRAME_BYTES;
|
|
47
|
+
|
|
48
|
+
export class EventOutbox {
|
|
49
|
+
private entries: OutboxEntry[] = [];
|
|
50
|
+
private totalBytes = 0;
|
|
51
|
+
private nextSeq = 1;
|
|
52
|
+
/** Highest seq ever transmitted on any connection. */
|
|
53
|
+
private sentUpTo = 0;
|
|
54
|
+
/** Entries dropped by overflow since the last drain — for one loud log. */
|
|
55
|
+
private droppedSinceDrain = 0;
|
|
56
|
+
|
|
57
|
+
/** Serialize + append. Returns the entry's seq. */
|
|
58
|
+
enqueue(event: HostEvent): number {
|
|
59
|
+
const seq = this.nextSeq++;
|
|
60
|
+
const raw = JSON.stringify({ kind: "event", seq, event });
|
|
61
|
+
const bytes = Buffer.byteLength(raw, "utf8");
|
|
62
|
+
if (bytes > MAX_EVENT_FRAME_BYTES) {
|
|
63
|
+
this.droppedSinceDrain += 1;
|
|
64
|
+
return seq;
|
|
65
|
+
}
|
|
66
|
+
this.entries.push({ seq, raw, bytes });
|
|
67
|
+
this.totalBytes += bytes;
|
|
68
|
+
while (
|
|
69
|
+
this.entries.length > MAX_ENTRIES ||
|
|
70
|
+
(this.totalBytes > MAX_BYTES && this.entries.length > 1)
|
|
71
|
+
) {
|
|
72
|
+
const dropped = this.entries.shift();
|
|
73
|
+
if (!dropped) break;
|
|
74
|
+
this.totalBytes -= dropped.bytes;
|
|
75
|
+
// Only a NEVER-SENT entry is a real loss; an unacked-but-sent one most
|
|
76
|
+
// likely landed and just lost its ack.
|
|
77
|
+
if (dropped.seq > this.sentUpTo) this.droppedSinceDrain += 1;
|
|
78
|
+
}
|
|
79
|
+
return seq;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Cumulative ack: forget everything up to and including `seq`. */
|
|
83
|
+
ack(seq: number): void {
|
|
84
|
+
let i = 0;
|
|
85
|
+
for (const entry of this.entries) {
|
|
86
|
+
if (entry.seq > seq) break;
|
|
87
|
+
this.totalBytes -= entry.bytes;
|
|
88
|
+
i += 1;
|
|
89
|
+
}
|
|
90
|
+
if (i > 0) this.entries.splice(0, i);
|
|
91
|
+
if (seq > this.sentUpTo) this.sentUpTo = seq;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Rewind the transmit cursor for a reconnect (ADR-103 resume handshake).
|
|
96
|
+
* `afterSeq` is the cloud's watermark: everything newer re-sends on the
|
|
97
|
+
* next drain. `null` = the cloud has no state for this boot — replay
|
|
98
|
+
* nothing; only never-transmitted entries go out.
|
|
99
|
+
*/
|
|
100
|
+
resume(afterSeq: number | null): void {
|
|
101
|
+
if (afterSeq !== null && afterSeq < this.sentUpTo) this.sentUpTo = afterSeq;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Entries due for transmission, in order. The caller sends them and the
|
|
106
|
+
* cursor advances — drain is idempotent per entry until `resume` rewinds.
|
|
107
|
+
*/
|
|
108
|
+
drain(): OutboxEntry[] {
|
|
109
|
+
const due = this.entries.filter((e) => e.seq > this.sentUpTo);
|
|
110
|
+
const last = due[due.length - 1];
|
|
111
|
+
if (last) this.sentUpTo = last.seq;
|
|
112
|
+
return due;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Oldest due entry without advancing the transmit cursor. */
|
|
116
|
+
nextDue(): OutboxEntry | null {
|
|
117
|
+
return this.entries.find((entry) => entry.seq > this.sentUpTo) ?? null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Advance only the exact next due entry after the caller queues it. */
|
|
121
|
+
markSent(seq: number): boolean {
|
|
122
|
+
const due = this.nextDue();
|
|
123
|
+
if (!due || due.seq !== seq) return false;
|
|
124
|
+
this.sentUpTo = seq;
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Never-sent entries lost to overflow since the last call; resets. */
|
|
129
|
+
takeDroppedCount(): number {
|
|
130
|
+
const n = this.droppedSinceDrain;
|
|
131
|
+
this.droppedSinceDrain = 0;
|
|
132
|
+
return n;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
get size(): number {
|
|
136
|
+
return this.entries.length;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
get bytes(): number {
|
|
140
|
+
return this.totalBytes;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Queue due frames only until the transport reaches its high-water mark.
|
|
146
|
+
* Entries advance one at a time after `send` accepts them, so a slow socket
|
|
147
|
+
* leaves the remainder replayable in the bounded outbox instead of moving the
|
|
148
|
+
* whole backlog into the WebSocket implementation's own buffer.
|
|
149
|
+
*/
|
|
150
|
+
export function transmitEventOutbox(
|
|
151
|
+
outbox: EventOutbox,
|
|
152
|
+
sink: EventOutboxSink,
|
|
153
|
+
maxBufferedBytes: number,
|
|
154
|
+
): EventOutboxTransmitResult {
|
|
155
|
+
let projectedBufferedBytes = sink.bufferedAmount;
|
|
156
|
+
while (true) {
|
|
157
|
+
const entry = outbox.nextDue();
|
|
158
|
+
if (!entry) return "empty";
|
|
159
|
+
if (
|
|
160
|
+
maxBufferedBytes <= 0 ||
|
|
161
|
+
(projectedBufferedBytes > 0 &&
|
|
162
|
+
(projectedBufferedBytes >= maxBufferedBytes ||
|
|
163
|
+
entry.bytes > maxBufferedBytes - projectedBufferedBytes))
|
|
164
|
+
) {
|
|
165
|
+
return "backpressured";
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
sink.send(entry.raw);
|
|
169
|
+
} catch {
|
|
170
|
+
return "send_failed";
|
|
171
|
+
}
|
|
172
|
+
if (!outbox.markSent(entry.seq)) return "send_failed";
|
|
173
|
+
projectedBufferedBytes = Math.max(
|
|
174
|
+
sink.bufferedAmount,
|
|
175
|
+
projectedBufferedBytes + entry.bytes,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// A rolling-compatible frame above the normal high-water budget gets one
|
|
179
|
+
// empty transport to itself. Do not depend on a sink updating
|
|
180
|
+
// `bufferedAmount` synchronously before protecting the following frame.
|
|
181
|
+
if (entry.bytes > maxBufferedBytes) {
|
|
182
|
+
return outbox.nextDue() ? "backpressured" : "empty";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|