@intx/hub-sessions 0.1.2 → 0.3.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 +176 -0
- package/README.md +82 -1
- package/dist/agent-repo.d.ts +93 -0
- package/dist/agent-repo.js +109 -0
- package/dist/agent-state-kind.d.ts +12 -0
- package/dist/agent-state-kind.js +189 -0
- package/dist/asset-service.d.ts +104 -0
- package/dist/asset-service.js +267 -0
- package/dist/committed-source-tree.d.ts +10 -0
- package/dist/committed-source-tree.js +35 -0
- package/dist/credential-push.d.ts +33 -0
- package/dist/credential-push.js +109 -0
- package/dist/event-collector-registry.d.ts +20 -0
- package/dist/event-collector-registry.js +115 -0
- package/dist/event-collector.d.ts +39 -0
- package/dist/event-collector.js +365 -0
- package/dist/hub-session-lookups.d.ts +135 -0
- package/dist/hub-session-lookups.js +663 -0
- package/dist/hub-session-orchestrator.d.ts +25 -0
- package/dist/hub-session-orchestrator.js +87 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +24 -0
- package/dist/package-registry-kind.d.ts +70 -0
- package/dist/package-registry-kind.js +260 -0
- package/dist/repo-store/index.d.ts +4 -0
- package/dist/repo-store/index.js +3 -0
- package/dist/repo-store/store.d.ts +41 -0
- package/dist/repo-store/store.js +1829 -0
- package/dist/repo-store/subscribe-kind.d.ts +56 -0
- package/dist/repo-store/subscribe-kind.js +144 -0
- package/dist/repo-store/types.d.ts +571 -0
- package/dist/repo-store/types.js +42 -0
- package/dist/session-service.d.ts +416 -0
- package/dist/session-service.js +1191 -0
- package/dist/sidecar-allocation/contracts.d.ts +78 -0
- package/dist/sidecar-allocation/contracts.js +21 -0
- package/dist/sidecar-allocation/index.d.ts +4 -0
- package/dist/sidecar-allocation/index.js +3 -0
- package/dist/sidecar-allocation/placement-policy.d.ts +11 -0
- package/dist/sidecar-allocation/placement-policy.js +21 -0
- package/dist/sidecar-allocation/plugin-registry.d.ts +11 -0
- package/dist/sidecar-allocation/plugin-registry.js +37 -0
- package/dist/sidecar-allocation/reconciler.d.ts +42 -0
- package/dist/sidecar-allocation/reconciler.js +431 -0
- package/dist/skill-kind.d.ts +41 -0
- package/dist/skill-kind.js +292 -0
- package/dist/substrate.d.ts +8 -0
- package/dist/substrate.js +21 -0
- package/dist/workflow-allocation-service.d.ts +58 -0
- package/dist/workflow-allocation-service.js +239 -0
- package/dist/workflow-closure-resolution.d.ts +106 -0
- package/dist/workflow-closure-resolution.js +123 -0
- package/dist/workflow-definition-ensure.d.ts +24 -0
- package/dist/workflow-definition-ensure.js +75 -0
- package/dist/workflow-dispatch-service.d.ts +40 -0
- package/dist/workflow-dispatch-service.js +146 -0
- package/dist/workflow-dispatch-settlement.d.ts +29 -0
- package/dist/workflow-dispatch-settlement.js +140 -0
- package/dist/workflow-kind.d.ts +37 -0
- package/dist/workflow-kind.js +310 -0
- package/dist/workflow-probe-gate.d.ts +214 -0
- package/dist/workflow-probe-gate.js +207 -0
- package/dist/workflow-run-event-log.d.ts +21 -0
- package/dist/workflow-run-event-log.js +51 -0
- package/dist/workflow-run-kind.d.ts +440 -0
- package/dist/workflow-run-kind.js +2916 -0
- package/dist/workflow-run-reader.d.ts +47 -0
- package/dist/workflow-run-reader.js +153 -0
- package/dist/workflow-run-restore.d.ts +15 -0
- package/dist/workflow-run-restore.js +26 -0
- package/dist/workflow-source-closure.d.ts +35 -0
- package/dist/workflow-source-closure.js +342 -0
- package/dist/ws/index.d.ts +3 -0
- package/dist/ws/index.js +3 -0
- package/dist/ws/sidecar-events.d.ts +222 -0
- package/dist/ws/sidecar-events.js +72 -0
- package/dist/ws/sidecar-handler.d.ts +305 -0
- package/dist/ws/sidecar-handler.js +2537 -0
- package/dist/ws/sidecar-token-authenticator.d.ts +17 -0
- package/dist/ws/sidecar-token-authenticator.js +81 -0
- package/package.json +35 -12
- package/src/agent-repo.test.ts +0 -310
- package/src/agent-repo.ts +0 -165
- package/src/agent-state-kind.test.ts +0 -247
- package/src/agent-state-kind.ts +0 -204
- package/src/asset-service.test.ts +0 -540
- package/src/asset-service.ts +0 -378
- package/src/available-skills-stanza.test.ts +0 -87
- package/src/available-skills-stanza.ts +0 -47
- package/src/credential-push.ts +0 -65
- package/src/event-collector-registry.test.ts +0 -73
- package/src/event-collector-registry.ts +0 -171
- package/src/event-collector.test.ts +0 -1387
- package/src/event-collector.ts +0 -424
- package/src/hub-session-lookups.ts +0 -206
- package/src/hub-session-orchestrator.test.ts +0 -510
- package/src/hub-session-orchestrator.ts +0 -213
- package/src/index.ts +0 -78
- package/src/repo-store/index.ts +0 -15
- package/src/repo-store/store.test.ts +0 -1169
- package/src/repo-store/store.ts +0 -428
- package/src/repo-store/types.ts +0 -253
- package/src/session-service.test.ts +0 -895
- package/src/session-service.ts +0 -464
- package/src/skill-kind.test.ts +0 -599
- package/src/skill-kind.ts +0 -350
- package/src/ws/index.ts +0 -18
- package/src/ws/sidecar-events.test.ts +0 -96
- package/src/ws/sidecar-events.ts +0 -231
- package/src/ws/sidecar-handler.test.ts +0 -2217
- package/src/ws/sidecar-handler.ts +0 -1574
- package/tsconfig.json +0 -4
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type PackumentFetcher, type RegistryConfig } from "@intx/tool-packaging";
|
|
2
|
+
import type { WorkflowSourceAssetMount } from "@intx/types/sidecar";
|
|
3
|
+
import type { ToolPackageManifest } from "@intx/types/tool-packages";
|
|
4
|
+
import type { WorkflowDefinitionAssetSource, WorkflowDefinitionRegistrySource } from "@intx/types/workflow-sources";
|
|
5
|
+
import { type SourceTreeReads } from "./workflow-source-closure.js";
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a workflow definition's dependency closure against the npm registry
|
|
8
|
+
* the source names.
|
|
9
|
+
*/
|
|
10
|
+
export type ResolveWorkflowClosureRegistryArgs = {
|
|
11
|
+
readonly source: WorkflowDefinitionRegistrySource;
|
|
12
|
+
/** A `name@range` spec for the workflow definition package. */
|
|
13
|
+
readonly pin: string;
|
|
14
|
+
/** URL and credentials for the registry named by `source.registry`. */
|
|
15
|
+
readonly registryConfig: RegistryConfig;
|
|
16
|
+
/**
|
|
17
|
+
* Test seam for packument fetches. Omitted in production, where the
|
|
18
|
+
* default `npm-registry-fetch` wrapper reaches the configured URL.
|
|
19
|
+
*/
|
|
20
|
+
readonly fetchPackument?: PackumentFetcher;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a workflow definition's dependency closure against a single hub
|
|
24
|
+
* `package-registry` asset.
|
|
25
|
+
*
|
|
26
|
+
* The resolver is single-source by design: the asset is the sole and default
|
|
27
|
+
* registry, so a self-contained definition (its `@intx/*` imports bundled at
|
|
28
|
+
* author time) resolves to one `kind:"asset"` entry. A specifier the asset does
|
|
29
|
+
* not publish fails loud from `AssetRegistrySource` -- there is no silent
|
|
30
|
+
* fallback to npm. A definition that genuinely needs external npm dependencies
|
|
31
|
+
* would resolve through the mixed `AssetRegistrySource` + `HttpRegistrySource`
|
|
32
|
+
* map `session-service` already builds for tool packages; that path is not
|
|
33
|
+
* threaded here.
|
|
34
|
+
*/
|
|
35
|
+
export type ResolveWorkflowClosureAssetTarballArgs = {
|
|
36
|
+
/** An asset source whose `package.format` is `"tarball"`. */
|
|
37
|
+
readonly source: WorkflowDefinitionAssetSource;
|
|
38
|
+
/** A `name@range` spec for the workflow definition package. */
|
|
39
|
+
readonly pin: string;
|
|
40
|
+
/** Reads a blob at `path` from the asset the definition is sourced from. */
|
|
41
|
+
readonly readBlob: (path: string) => Promise<Uint8Array>;
|
|
42
|
+
/** Lists the blob names directly under `dir` in that asset. */
|
|
43
|
+
readonly listBlobs: (dir: string) => Promise<string[]>;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a source-format workflow definition's dependency closure from the
|
|
47
|
+
* asset's git tree at the pinned commit. The workspace-local members become
|
|
48
|
+
* `format:"source"` entries and the external npm deps resolve through the
|
|
49
|
+
* npm registry the caller supplies; there is no `name@range` pin (the member
|
|
50
|
+
* is selected from `source.package.packageName`).
|
|
51
|
+
*/
|
|
52
|
+
export type ResolveWorkflowClosureAssetSourceArgs = {
|
|
53
|
+
/** An asset source whose `package.format` is `"source"`. */
|
|
54
|
+
readonly source: WorkflowDefinitionAssetSource;
|
|
55
|
+
/** Git-tree reads pinned to `source.package.commitSha`. */
|
|
56
|
+
readonly reads: SourceTreeReads;
|
|
57
|
+
/**
|
|
58
|
+
* The registry name external deps are stamped with in the frozen closure.
|
|
59
|
+
* Must be a name the sidecar's registry map is keyed by (its npm registry),
|
|
60
|
+
* since the sidecar resolves each external entry's `source.registry` against
|
|
61
|
+
* that map at materialization.
|
|
62
|
+
*/
|
|
63
|
+
readonly registryName: string;
|
|
64
|
+
/** URL and credentials for the npm registry external deps resolve against. */
|
|
65
|
+
readonly registryConfig: RegistryConfig;
|
|
66
|
+
/**
|
|
67
|
+
* Test seam for packument fetches. Omitted in production, where the
|
|
68
|
+
* default `npm-registry-fetch` wrapper reaches the configured URL.
|
|
69
|
+
*/
|
|
70
|
+
readonly fetchPackument?: PackumentFetcher;
|
|
71
|
+
};
|
|
72
|
+
export type ResolveWorkflowClosureArgs = ResolveWorkflowClosureRegistryArgs | ResolveWorkflowClosureAssetTarballArgs | ResolveWorkflowClosureAssetSourceArgs;
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a workflow definition's dependency closure to concrete versions
|
|
75
|
+
* and integrity SRIs, reusing the tool-packaging closure resolver.
|
|
76
|
+
*
|
|
77
|
+
* @returns the frozen closure: a `ToolPackageManifest` whose `topLevel`
|
|
78
|
+
* pins the definition package and whose `entries` carry the concrete
|
|
79
|
+
* version and integrity SRI of every transitive dependency.
|
|
80
|
+
*/
|
|
81
|
+
export declare function resolveWorkflowClosure(args: ResolveWorkflowClosureArgs): Promise<ToolPackageManifest>;
|
|
82
|
+
/**
|
|
83
|
+
* The workspace-relative mount path a workflow definition's source asset is
|
|
84
|
+
* delivered under, keyed by asset id. Both the probe (inline delivery) and the
|
|
85
|
+
* deploy (streamed fan-out) build their `assetId -> mountPath` maps through
|
|
86
|
+
* this one helper, so the same asset lands at the same path on both paths.
|
|
87
|
+
*/
|
|
88
|
+
export declare function workflowSourceAssetMountPath(assetId: string): string;
|
|
89
|
+
/**
|
|
90
|
+
* Resolves an asset (by id) to the git pack a source-ref frame delivers for it.
|
|
91
|
+
* The caller binds the ref: an asset source binds the asset's default ref, so
|
|
92
|
+
* the install/deploy glue stays ref-agnostic.
|
|
93
|
+
*/
|
|
94
|
+
export type ResolveAssetAttachmentFn = (assetId: string) => Promise<{
|
|
95
|
+
pack: Uint8Array;
|
|
96
|
+
ref: string;
|
|
97
|
+
commitSha: string;
|
|
98
|
+
}>;
|
|
99
|
+
/**
|
|
100
|
+
* Build the inline asset mounts a source-ref frame delivers for an asset-sourced
|
|
101
|
+
* closure: one per distinct asset the closure's `kind:"asset"` entries name (so
|
|
102
|
+
* a multi-asset closure delivers every backing asset, not just the source), each
|
|
103
|
+
* carrying the git pack the caller resolves for that asset. `mountPath` is
|
|
104
|
+
* derived through the shared helper so probe and deploy agree on it.
|
|
105
|
+
*/
|
|
106
|
+
export declare function buildSourceAssetMounts(closure: ToolPackageManifest, resolveAttachment: ResolveAssetAttachmentFn): Promise<WorkflowSourceAssetMount[]>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Hub-side dependency-closure resolution for a code-sourced workflow
|
|
2
|
+
// definition.
|
|
3
|
+
//
|
|
4
|
+
// Given a `WorkflowDefinitionSource` and a pin, walk the full dependency
|
|
5
|
+
// closure to concrete versions plus integrity SRIs, reusing the tool-packaging
|
|
6
|
+
// closure resolver. This is the "what exactly would we install" step: packument
|
|
7
|
+
// and package.json metadata reads only, no code execution. The returned
|
|
8
|
+
// `ToolPackageManifest` is the frozen closure the deploy path later ships by
|
|
9
|
+
// source-ref.
|
|
10
|
+
//
|
|
11
|
+
// There are three arms, one per `(source.kind, package.format)`:
|
|
12
|
+
// - `registry` names an npm registry; its URL and credentials are supplied by
|
|
13
|
+
// the caller as a `RegistryConfig`, since the caller owns the registry
|
|
14
|
+
// configuration.
|
|
15
|
+
// - `asset` + `tarball` names a hub asset that publishes the definition
|
|
16
|
+
// package as a tarball; the caller supplies bound `readBlob`/`listBlobs`
|
|
17
|
+
// closures over that asset (the same shape `session-service` mints for
|
|
18
|
+
// `AssetRegistrySource`).
|
|
19
|
+
// - `asset` + `source` names a hub asset whose definition package lives as a
|
|
20
|
+
// git subtree at a pinned commit; the caller supplies a `SourceTreeReads`
|
|
21
|
+
// pinned to that commit plus a `RegistryConfig` for the external npm deps.
|
|
22
|
+
// This arm delegates to `resolveSourceWorkflowClosure`, which reads the
|
|
23
|
+
// tree directly and needs no `name@range` pin (it selects the member from
|
|
24
|
+
// the source's `packageName`).
|
|
25
|
+
// Every arm takes pre-bound read capabilities, keeping this module free of any
|
|
26
|
+
// asset-service or repo-store dependency.
|
|
27
|
+
import { base64Encode } from "@intx/types";
|
|
28
|
+
import { AssetRegistrySource, HttpRegistrySource, createClosureResolver, parsePin, } from "@intx/tool-packaging";
|
|
29
|
+
import { resolveSourceWorkflowClosure, } from "./workflow-source-closure.js";
|
|
30
|
+
// Both asset arms carry an identical `source` field type, so narrow on the
|
|
31
|
+
// source's own `package.format` discriminant rather than adding a redundant
|
|
32
|
+
// discriminant to the args.
|
|
33
|
+
function isAssetSourceArgs(args) {
|
|
34
|
+
return (args.source.kind === "asset" && args.source.package.format === "source");
|
|
35
|
+
}
|
|
36
|
+
function isAssetTarballArgs(args) {
|
|
37
|
+
return (args.source.kind === "asset" && args.source.package.format === "tarball");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a workflow definition's dependency closure to concrete versions
|
|
41
|
+
* and integrity SRIs, reusing the tool-packaging closure resolver.
|
|
42
|
+
*
|
|
43
|
+
* @returns the frozen closure: a `ToolPackageManifest` whose `topLevel`
|
|
44
|
+
* pins the definition package and whose `entries` carry the concrete
|
|
45
|
+
* version and integrity SRI of every transitive dependency.
|
|
46
|
+
*/
|
|
47
|
+
export async function resolveWorkflowClosure(args) {
|
|
48
|
+
if (isAssetSourceArgs(args)) {
|
|
49
|
+
return resolveSourceWorkflowClosure({
|
|
50
|
+
source: args.source,
|
|
51
|
+
reads: args.reads,
|
|
52
|
+
registryName: args.registryName,
|
|
53
|
+
registryConfig: args.registryConfig,
|
|
54
|
+
...(args.fetchPackument !== undefined
|
|
55
|
+
? { fetchPackument: args.fetchPackument }
|
|
56
|
+
: {}),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (isAssetTarballArgs(args)) {
|
|
60
|
+
// The assetId is the source name so every walker resolution error is
|
|
61
|
+
// traceable to the exact asset; it is the sole and default source.
|
|
62
|
+
const name = args.source.assetId;
|
|
63
|
+
const assetSource = new AssetRegistrySource({
|
|
64
|
+
name,
|
|
65
|
+
assetId: args.source.assetId,
|
|
66
|
+
readBlob: args.readBlob,
|
|
67
|
+
listBlobs: args.listBlobs,
|
|
68
|
+
});
|
|
69
|
+
const resolver = createClosureResolver({
|
|
70
|
+
registries: new Map([[name, assetSource]]),
|
|
71
|
+
defaultRegistry: name,
|
|
72
|
+
});
|
|
73
|
+
return resolver.resolveClosure([parsePin(args.pin)]);
|
|
74
|
+
}
|
|
75
|
+
const registrySource = new HttpRegistrySource({
|
|
76
|
+
name: args.source.registry,
|
|
77
|
+
config: args.registryConfig,
|
|
78
|
+
...(args.fetchPackument !== undefined
|
|
79
|
+
? { fetchPackument: args.fetchPackument }
|
|
80
|
+
: {}),
|
|
81
|
+
});
|
|
82
|
+
const resolver = createClosureResolver({
|
|
83
|
+
registries: new Map([[registrySource.name, registrySource]]),
|
|
84
|
+
defaultRegistry: registrySource.name,
|
|
85
|
+
});
|
|
86
|
+
return resolver.resolveClosure([parsePin(args.pin)]);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The workspace-relative mount path a workflow definition's source asset is
|
|
90
|
+
* delivered under, keyed by asset id. Both the probe (inline delivery) and the
|
|
91
|
+
* deploy (streamed fan-out) build their `assetId -> mountPath` maps through
|
|
92
|
+
* this one helper, so the same asset lands at the same path on both paths.
|
|
93
|
+
*/
|
|
94
|
+
export function workflowSourceAssetMountPath(assetId) {
|
|
95
|
+
return `source-assets/${assetId}/`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Build the inline asset mounts a source-ref frame delivers for an asset-sourced
|
|
99
|
+
* closure: one per distinct asset the closure's `kind:"asset"` entries name (so
|
|
100
|
+
* a multi-asset closure delivers every backing asset, not just the source), each
|
|
101
|
+
* carrying the git pack the caller resolves for that asset. `mountPath` is
|
|
102
|
+
* derived through the shared helper so probe and deploy agree on it.
|
|
103
|
+
*/
|
|
104
|
+
export async function buildSourceAssetMounts(closure, resolveAttachment) {
|
|
105
|
+
const assetIds = new Set();
|
|
106
|
+
for (const entry of closure.entries) {
|
|
107
|
+
if (entry.source.kind === "asset") {
|
|
108
|
+
assetIds.add(entry.source.assetId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const mounts = [];
|
|
112
|
+
for (const assetId of assetIds) {
|
|
113
|
+
const attachment = await resolveAttachment(assetId);
|
|
114
|
+
mounts.push({
|
|
115
|
+
assetId,
|
|
116
|
+
mountPath: workflowSourceAssetMountPath(assetId),
|
|
117
|
+
pack: base64Encode(attachment.pack),
|
|
118
|
+
ref: attachment.ref,
|
|
119
|
+
commitSha: attachment.commitSha,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return mounts;
|
|
123
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type DBExecutor, type WorkflowDefinitionSelector } from "@intx/db";
|
|
2
|
+
/**
|
|
3
|
+
* The first-class `workflow_definition` a selector names, created if absent. A
|
|
4
|
+
* native workflow carries its body in the asset it points at, so this only
|
|
5
|
+
* projects a definition row -- plus its version "1" -- over that asset, keyed
|
|
6
|
+
* by the selector's wire hash, so a run can anchor on a first-class definition.
|
|
7
|
+
* A single asset backs many definitions distinguished by their wire hash, so
|
|
8
|
+
* distinct hashes over one asset yield distinct definitions.
|
|
9
|
+
* `currentVersion`/`status` are left to the table defaults; the definition
|
|
10
|
+
* carries no `modelRequirements` manifest, so it deploys as a workflow rather
|
|
11
|
+
* than launching as an instance.
|
|
12
|
+
*
|
|
13
|
+
* Idempotent and concurrency-safe: the definition insert conflicts on the
|
|
14
|
+
* unique `(assetId, wireHash)` and the version on `(definitionId, version)`, so
|
|
15
|
+
* two concurrent deploys of the same selector still yield exactly one
|
|
16
|
+
* definition and one version. The returned id is read back after the conflict
|
|
17
|
+
* -- the winner's id, not a locally-minted one -- so a losing insert anchors
|
|
18
|
+
* its version on the real row. `created` distinguishes a fresh projection from
|
|
19
|
+
* a pre-existing one for callers that report fold counts.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ensureWorkflowDefinitionForAsset(db: DBExecutor, selector: WorkflowDefinitionSelector): Promise<{
|
|
22
|
+
definitionId: string;
|
|
23
|
+
created: boolean;
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { resolveDefinitionIdForAsset, } from "@intx/db";
|
|
3
|
+
import { asset, workflowDefinition, workflowDefinitionVersion, } from "@intx/db/schema";
|
|
4
|
+
import { generateId } from "@intx/hub-common";
|
|
5
|
+
/**
|
|
6
|
+
* The first-class `workflow_definition` a selector names, created if absent. A
|
|
7
|
+
* native workflow carries its body in the asset it points at, so this only
|
|
8
|
+
* projects a definition row -- plus its version "1" -- over that asset, keyed
|
|
9
|
+
* by the selector's wire hash, so a run can anchor on a first-class definition.
|
|
10
|
+
* A single asset backs many definitions distinguished by their wire hash, so
|
|
11
|
+
* distinct hashes over one asset yield distinct definitions.
|
|
12
|
+
* `currentVersion`/`status` are left to the table defaults; the definition
|
|
13
|
+
* carries no `modelRequirements` manifest, so it deploys as a workflow rather
|
|
14
|
+
* than launching as an instance.
|
|
15
|
+
*
|
|
16
|
+
* Idempotent and concurrency-safe: the definition insert conflicts on the
|
|
17
|
+
* unique `(assetId, wireHash)` and the version on `(definitionId, version)`, so
|
|
18
|
+
* two concurrent deploys of the same selector still yield exactly one
|
|
19
|
+
* definition and one version. The returned id is read back after the conflict
|
|
20
|
+
* -- the winner's id, not a locally-minted one -- so a losing insert anchors
|
|
21
|
+
* its version on the real row. `created` distinguishes a fresh projection from
|
|
22
|
+
* a pre-existing one for callers that report fold counts.
|
|
23
|
+
*/
|
|
24
|
+
export async function ensureWorkflowDefinitionForAsset(db, selector) {
|
|
25
|
+
const { assetId, wireHash } = selector;
|
|
26
|
+
const assetRow = await db
|
|
27
|
+
.select({
|
|
28
|
+
tenantId: asset.tenantId,
|
|
29
|
+
creatorPrincipalId: asset.creatorPrincipalId,
|
|
30
|
+
name: asset.name,
|
|
31
|
+
displayName: asset.displayName,
|
|
32
|
+
})
|
|
33
|
+
.from(asset)
|
|
34
|
+
.where(eq(asset.id, assetId))
|
|
35
|
+
.limit(1)
|
|
36
|
+
.then((rows) => rows[0]);
|
|
37
|
+
if (assetRow === undefined) {
|
|
38
|
+
throw new Error(`ensureWorkflowDefinitionForAsset: no asset found for id "${assetId}"`);
|
|
39
|
+
}
|
|
40
|
+
const inserted = await db
|
|
41
|
+
.insert(workflowDefinition)
|
|
42
|
+
.values({
|
|
43
|
+
id: generateId("workflowDefinition"),
|
|
44
|
+
tenantId: assetRow.tenantId,
|
|
45
|
+
creatorPrincipalId: assetRow.creatorPrincipalId,
|
|
46
|
+
assetId,
|
|
47
|
+
wireHash,
|
|
48
|
+
name: assetRow.name,
|
|
49
|
+
description: assetRow.displayName,
|
|
50
|
+
})
|
|
51
|
+
.onConflictDoNothing({
|
|
52
|
+
target: [workflowDefinition.assetId, workflowDefinition.wireHash],
|
|
53
|
+
})
|
|
54
|
+
.returning({ id: workflowDefinition.id });
|
|
55
|
+
const created = inserted.length > 0;
|
|
56
|
+
const insertedId = inserted[0]?.id;
|
|
57
|
+
const definitionId = insertedId ?? (await resolveDefinitionIdForAsset(db, selector));
|
|
58
|
+
if (definitionId === null || definitionId === undefined) {
|
|
59
|
+
throw new Error(`ensureWorkflowDefinitionForAsset: definition for asset "${assetId}" (wire hash "${wireHash}") missing after insert`);
|
|
60
|
+
}
|
|
61
|
+
await db
|
|
62
|
+
.insert(workflowDefinitionVersion)
|
|
63
|
+
.values({
|
|
64
|
+
id: generateId("workflowDefinitionVersion"),
|
|
65
|
+
definitionId,
|
|
66
|
+
version: "1",
|
|
67
|
+
})
|
|
68
|
+
.onConflictDoNothing({
|
|
69
|
+
target: [
|
|
70
|
+
workflowDefinitionVersion.definitionId,
|
|
71
|
+
workflowDefinitionVersion.version,
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
return { definitionId, created };
|
|
75
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DBExecutor, EnqueueWorkflowRunDispatchArgs, EnqueueWorkflowRunDispatchResult, EnqueueWorkflowSignalDispatchArgs, SidecarAllocationStore, WorkflowRunDispatchStore } from "@intx/db";
|
|
2
|
+
import type { SidecarAllocationRouter } from "./ws/sidecar-handler.js";
|
|
3
|
+
type DispatchStore = Pick<WorkflowRunDispatchStore, "acknowledge" | "claimNextPending" | "enqueue" | "enqueueSignal" | "requeueUnsettled" | "scheduleRetry" | "settle">;
|
|
4
|
+
type AllocationStore = Pick<SidecarAllocationStore, "findByAnchorRunId">;
|
|
5
|
+
type DispatchRouter = Pick<SidecarAllocationRouter, "sendSignalDeliverToAllocation" | "sendWorkflowRunDispatchToAllocation">;
|
|
6
|
+
export type WorkflowDispatchAcknowledgement = {
|
|
7
|
+
readonly allocationId: string;
|
|
8
|
+
readonly anchorRunId: string;
|
|
9
|
+
readonly generation: number;
|
|
10
|
+
readonly messageId: string;
|
|
11
|
+
};
|
|
12
|
+
export type WorkflowDispatchService = {
|
|
13
|
+
enqueue(args: EnqueueWorkflowRunDispatchArgs, tx?: DBExecutor): Promise<EnqueueWorkflowRunDispatchResult>;
|
|
14
|
+
enqueueSignal(args: EnqueueWorkflowSignalDispatchArgs, tx?: DBExecutor): Promise<EnqueueWorkflowRunDispatchResult>;
|
|
15
|
+
acknowledge(args: WorkflowDispatchAcknowledgement): Promise<void>;
|
|
16
|
+
settle(anchorRunId: string, messageId: string): Promise<void>;
|
|
17
|
+
requeueForReadyAllocation(anchorRunId: string): Promise<number>;
|
|
18
|
+
reconcileNext(): Promise<boolean>;
|
|
19
|
+
reconcileUntilIdle(maxIterations?: number): Promise<number>;
|
|
20
|
+
wake(): void;
|
|
21
|
+
};
|
|
22
|
+
export type WorkflowDispatchServiceDeps = {
|
|
23
|
+
readonly dispatchStore: DispatchStore;
|
|
24
|
+
readonly allocationStore: AllocationStore;
|
|
25
|
+
readonly router: DispatchRouter;
|
|
26
|
+
/** Resolve the deployment anchor's durable routing address. */
|
|
27
|
+
readonly resolveAnchorAddress: (anchorRunId: string) => Promise<string | null>;
|
|
28
|
+
readonly leaseDurationMs?: number;
|
|
29
|
+
readonly retryDelayMs?: (attempt: number) => number;
|
|
30
|
+
readonly now?: () => Date;
|
|
31
|
+
readonly createLeaseId?: () => string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Drives Hub-owned workflow triggers onto exclusive sidecars. The database
|
|
35
|
+
* row is the delivery authority: websocket acceptance never deletes the raw
|
|
36
|
+
* payload, and a generation replacement requeues every row that has not been
|
|
37
|
+
* settled by the workflow-run Git claim-check.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createWorkflowDispatchService({ dispatchStore, allocationStore, router, resolveAnchorAddress, leaseDurationMs, retryDelayMs, now, createLeaseId, }: WorkflowDispatchServiceDeps): WorkflowDispatchService;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { getLogger } from "@intx/log";
|
|
2
|
+
import { base64Encode, hexEncode } from "@intx/types";
|
|
3
|
+
import { SignalDeliverFrame } from "@intx/types/sidecar";
|
|
4
|
+
const logger = getLogger(["hub", "workflow-dispatch"]);
|
|
5
|
+
const DEFAULT_LEASE_DURATION_MS = 30_000;
|
|
6
|
+
function defaultRetryDelay(attempt) {
|
|
7
|
+
return Math.min(500 * 2 ** Math.min(attempt, 6), 30_000);
|
|
8
|
+
}
|
|
9
|
+
function randomLeaseId() {
|
|
10
|
+
return `dispatch_lease_${hexEncode(crypto.getRandomValues(new Uint8Array(16)))}`;
|
|
11
|
+
}
|
|
12
|
+
function targetForReadyAllocation(allocation) {
|
|
13
|
+
if (allocation.status !== "allocated" ||
|
|
14
|
+
allocation.ensureAcceptedGeneration !== allocation.generation ||
|
|
15
|
+
allocation.connectDeadline !== undefined) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
allocationId: allocation.id,
|
|
20
|
+
generation: allocation.generation,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Drives Hub-owned workflow triggers onto exclusive sidecars. The database
|
|
25
|
+
* row is the delivery authority: websocket acceptance never deletes the raw
|
|
26
|
+
* payload, and a generation replacement requeues every row that has not been
|
|
27
|
+
* settled by the workflow-run Git claim-check.
|
|
28
|
+
*/
|
|
29
|
+
export function createWorkflowDispatchService({ dispatchStore, allocationStore, router, resolveAnchorAddress, leaseDurationMs = DEFAULT_LEASE_DURATION_MS, retryDelayMs = defaultRetryDelay, now = () => new Date(), createLeaseId = randomLeaseId, }) {
|
|
30
|
+
if (leaseDurationMs <= 0) {
|
|
31
|
+
throw new Error("leaseDurationMs must be positive");
|
|
32
|
+
}
|
|
33
|
+
let drainPromise = null;
|
|
34
|
+
function retryAt(attempt) {
|
|
35
|
+
return new Date(now().getTime() + retryDelayMs(attempt));
|
|
36
|
+
}
|
|
37
|
+
async function retry(dispatch, leaseId, code, message) {
|
|
38
|
+
await dispatchStore.scheduleRetry({
|
|
39
|
+
dispatchId: dispatch.id,
|
|
40
|
+
nextAttemptAt: retryAt(dispatch.attemptCount),
|
|
41
|
+
code,
|
|
42
|
+
message,
|
|
43
|
+
expectedLeaseId: leaseId,
|
|
44
|
+
now: now(),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function reconcileNext() {
|
|
48
|
+
const leaseId = createLeaseId();
|
|
49
|
+
const dispatch = await dispatchStore.claimNextPending({
|
|
50
|
+
leaseId,
|
|
51
|
+
leaseDurationMs,
|
|
52
|
+
});
|
|
53
|
+
if (dispatch === null)
|
|
54
|
+
return false;
|
|
55
|
+
const allocation = await allocationStore.findByAnchorRunId(dispatch.anchorRunId);
|
|
56
|
+
if (allocation === null) {
|
|
57
|
+
await retry(dispatch, leaseId, "allocation_missing", `No sidecar allocation exists for workflow anchor ${dispatch.anchorRunId}`);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
const target = targetForReadyAllocation(allocation);
|
|
61
|
+
if (target === null) {
|
|
62
|
+
await retry(dispatch, leaseId, "allocation_not_ready", `Sidecar allocation ${allocation.id} is not ready for delivery`);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
const agentAddress = await resolveAnchorAddress(dispatch.anchorRunId);
|
|
66
|
+
if (agentAddress === null) {
|
|
67
|
+
await retry(dispatch, leaseId, "anchor_address_missing", `Workflow anchor ${dispatch.anchorRunId} has no routing address`);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
if (dispatch.kind === "signal") {
|
|
72
|
+
const signal = SignalDeliverFrame.assert(JSON.parse(new TextDecoder().decode(dispatch.rawMessage)));
|
|
73
|
+
await router.sendSignalDeliverToAllocation(target, {
|
|
74
|
+
agentAddress: signal.agentAddress,
|
|
75
|
+
runId: signal.runId,
|
|
76
|
+
signalName: signal.signalName,
|
|
77
|
+
signalId: signal.signalId,
|
|
78
|
+
payload: signal.payload,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
await router.sendWorkflowRunDispatchToAllocation(target, agentAddress,
|
|
83
|
+
// Every trigger of a deployment uses its stable mail address as the
|
|
84
|
+
// supervisor run id.
|
|
85
|
+
agentAddress, dispatch.stepGrants, base64Encode(dispatch.rawMessage), dispatch.messageId);
|
|
86
|
+
}
|
|
87
|
+
// Keep the delivery lease until the sidecar acknowledges its durable
|
|
88
|
+
// inbox write. If that ack never arrives, lease expiry makes the same
|
|
89
|
+
// immutable payload claimable again.
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
await retry(dispatch, leaseId, "dispatch_unroutable", error instanceof Error ? error.message : String(error));
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
async function reconcileUntilIdle(maxIterations = 100) {
|
|
97
|
+
let reconciled = 0;
|
|
98
|
+
while (reconciled < maxIterations && (await reconcileNext())) {
|
|
99
|
+
reconciled += 1;
|
|
100
|
+
}
|
|
101
|
+
return reconciled;
|
|
102
|
+
}
|
|
103
|
+
function wake() {
|
|
104
|
+
if (drainPromise !== null)
|
|
105
|
+
return;
|
|
106
|
+
drainPromise = Promise.resolve()
|
|
107
|
+
.then(async () => {
|
|
108
|
+
await reconcileUntilIdle();
|
|
109
|
+
})
|
|
110
|
+
.catch((error) => {
|
|
111
|
+
logger.error `Workflow dispatch reconciliation failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
112
|
+
})
|
|
113
|
+
.finally(() => {
|
|
114
|
+
drainPromise = null;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
async enqueue(args, tx) {
|
|
119
|
+
const result = await dispatchStore.enqueue(args, tx);
|
|
120
|
+
wake();
|
|
121
|
+
return result;
|
|
122
|
+
},
|
|
123
|
+
async enqueueSignal(args, tx) {
|
|
124
|
+
const result = await dispatchStore.enqueueSignal(args, tx);
|
|
125
|
+
wake();
|
|
126
|
+
return result;
|
|
127
|
+
},
|
|
128
|
+
async acknowledge(args) {
|
|
129
|
+
await dispatchStore.acknowledge({
|
|
130
|
+
...args,
|
|
131
|
+
now: now(),
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
async settle(anchorRunId, messageId) {
|
|
135
|
+
await dispatchStore.settle(anchorRunId, messageId, now());
|
|
136
|
+
},
|
|
137
|
+
async requeueForReadyAllocation(anchorRunId) {
|
|
138
|
+
const count = await dispatchStore.requeueUnsettled(anchorRunId);
|
|
139
|
+
wake();
|
|
140
|
+
return count;
|
|
141
|
+
},
|
|
142
|
+
reconcileNext,
|
|
143
|
+
reconcileUntilIdle,
|
|
144
|
+
wake,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CommittedReads } from "./repo-store/index.js";
|
|
2
|
+
export type ConsumedWorkflowDispatch = {
|
|
3
|
+
readonly messageId: string;
|
|
4
|
+
readonly address: string;
|
|
5
|
+
readonly rejection?: {
|
|
6
|
+
readonly code: string;
|
|
7
|
+
readonly message: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export type ReceivedWorkflowSignal = {
|
|
11
|
+
readonly runId: string;
|
|
12
|
+
readonly signalId: string;
|
|
13
|
+
};
|
|
14
|
+
export type AcceptedWorkflowDispatch = {
|
|
15
|
+
readonly runId: string;
|
|
16
|
+
readonly messageId: string;
|
|
17
|
+
readonly kind: "mail" | "signal";
|
|
18
|
+
};
|
|
19
|
+
/** Enumerate dispatches durably accepted by one live or sealed run log. */
|
|
20
|
+
export declare function listAcceptedWorkflowDispatches(reads: CommittedReads, runId: string, targetMessageIds?: ReadonlySet<string>): Promise<AcceptedWorkflowDispatch[]>;
|
|
21
|
+
/** Enumerate durable SignalReceived ids from one live or sealed run log. */
|
|
22
|
+
export declare function listReceivedWorkflowSignals(reads: CommittedReads, runId: string): Promise<ReceivedWorkflowSignal[]>;
|
|
23
|
+
/**
|
|
24
|
+
* Enumerate the workflow-run claim-check's retained consumed index. The kind
|
|
25
|
+
* validator guarantees this tree shape before the ref advances; the runtime
|
|
26
|
+
* validator here keeps the Git-to-database projection fail-closed if that
|
|
27
|
+
* invariant is ever violated.
|
|
28
|
+
*/
|
|
29
|
+
export declare function listConsumedWorkflowDispatches(reads: CommittedReads): Promise<ConsumedWorkflowDispatch[]>;
|