@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.
Files changed (113) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +82 -1
  3. package/dist/agent-repo.d.ts +93 -0
  4. package/dist/agent-repo.js +109 -0
  5. package/dist/agent-state-kind.d.ts +12 -0
  6. package/dist/agent-state-kind.js +189 -0
  7. package/dist/asset-service.d.ts +104 -0
  8. package/dist/asset-service.js +267 -0
  9. package/dist/committed-source-tree.d.ts +10 -0
  10. package/dist/committed-source-tree.js +35 -0
  11. package/dist/credential-push.d.ts +33 -0
  12. package/dist/credential-push.js +109 -0
  13. package/dist/event-collector-registry.d.ts +20 -0
  14. package/dist/event-collector-registry.js +115 -0
  15. package/dist/event-collector.d.ts +39 -0
  16. package/dist/event-collector.js +365 -0
  17. package/dist/hub-session-lookups.d.ts +135 -0
  18. package/dist/hub-session-lookups.js +663 -0
  19. package/dist/hub-session-orchestrator.d.ts +25 -0
  20. package/dist/hub-session-orchestrator.js +87 -0
  21. package/dist/index.d.ts +27 -0
  22. package/dist/index.js +24 -0
  23. package/dist/package-registry-kind.d.ts +70 -0
  24. package/dist/package-registry-kind.js +260 -0
  25. package/dist/repo-store/index.d.ts +4 -0
  26. package/dist/repo-store/index.js +3 -0
  27. package/dist/repo-store/store.d.ts +41 -0
  28. package/dist/repo-store/store.js +1829 -0
  29. package/dist/repo-store/subscribe-kind.d.ts +56 -0
  30. package/dist/repo-store/subscribe-kind.js +144 -0
  31. package/dist/repo-store/types.d.ts +571 -0
  32. package/dist/repo-store/types.js +42 -0
  33. package/dist/session-service.d.ts +416 -0
  34. package/dist/session-service.js +1191 -0
  35. package/dist/sidecar-allocation/contracts.d.ts +78 -0
  36. package/dist/sidecar-allocation/contracts.js +21 -0
  37. package/dist/sidecar-allocation/index.d.ts +4 -0
  38. package/dist/sidecar-allocation/index.js +3 -0
  39. package/dist/sidecar-allocation/placement-policy.d.ts +11 -0
  40. package/dist/sidecar-allocation/placement-policy.js +21 -0
  41. package/dist/sidecar-allocation/plugin-registry.d.ts +11 -0
  42. package/dist/sidecar-allocation/plugin-registry.js +37 -0
  43. package/dist/sidecar-allocation/reconciler.d.ts +42 -0
  44. package/dist/sidecar-allocation/reconciler.js +431 -0
  45. package/dist/skill-kind.d.ts +41 -0
  46. package/dist/skill-kind.js +292 -0
  47. package/dist/substrate.d.ts +8 -0
  48. package/dist/substrate.js +21 -0
  49. package/dist/workflow-allocation-service.d.ts +58 -0
  50. package/dist/workflow-allocation-service.js +239 -0
  51. package/dist/workflow-closure-resolution.d.ts +106 -0
  52. package/dist/workflow-closure-resolution.js +123 -0
  53. package/dist/workflow-definition-ensure.d.ts +24 -0
  54. package/dist/workflow-definition-ensure.js +75 -0
  55. package/dist/workflow-dispatch-service.d.ts +40 -0
  56. package/dist/workflow-dispatch-service.js +146 -0
  57. package/dist/workflow-dispatch-settlement.d.ts +29 -0
  58. package/dist/workflow-dispatch-settlement.js +140 -0
  59. package/dist/workflow-kind.d.ts +37 -0
  60. package/dist/workflow-kind.js +310 -0
  61. package/dist/workflow-probe-gate.d.ts +214 -0
  62. package/dist/workflow-probe-gate.js +207 -0
  63. package/dist/workflow-run-event-log.d.ts +21 -0
  64. package/dist/workflow-run-event-log.js +51 -0
  65. package/dist/workflow-run-kind.d.ts +440 -0
  66. package/dist/workflow-run-kind.js +2916 -0
  67. package/dist/workflow-run-reader.d.ts +47 -0
  68. package/dist/workflow-run-reader.js +153 -0
  69. package/dist/workflow-run-restore.d.ts +15 -0
  70. package/dist/workflow-run-restore.js +26 -0
  71. package/dist/workflow-source-closure.d.ts +35 -0
  72. package/dist/workflow-source-closure.js +342 -0
  73. package/dist/ws/index.d.ts +3 -0
  74. package/dist/ws/index.js +3 -0
  75. package/dist/ws/sidecar-events.d.ts +222 -0
  76. package/dist/ws/sidecar-events.js +72 -0
  77. package/dist/ws/sidecar-handler.d.ts +305 -0
  78. package/dist/ws/sidecar-handler.js +2537 -0
  79. package/dist/ws/sidecar-token-authenticator.d.ts +17 -0
  80. package/dist/ws/sidecar-token-authenticator.js +81 -0
  81. package/package.json +35 -12
  82. package/src/agent-repo.test.ts +0 -310
  83. package/src/agent-repo.ts +0 -165
  84. package/src/agent-state-kind.test.ts +0 -247
  85. package/src/agent-state-kind.ts +0 -204
  86. package/src/asset-service.test.ts +0 -540
  87. package/src/asset-service.ts +0 -378
  88. package/src/available-skills-stanza.test.ts +0 -87
  89. package/src/available-skills-stanza.ts +0 -47
  90. package/src/credential-push.ts +0 -65
  91. package/src/event-collector-registry.test.ts +0 -73
  92. package/src/event-collector-registry.ts +0 -171
  93. package/src/event-collector.test.ts +0 -1387
  94. package/src/event-collector.ts +0 -424
  95. package/src/hub-session-lookups.ts +0 -206
  96. package/src/hub-session-orchestrator.test.ts +0 -510
  97. package/src/hub-session-orchestrator.ts +0 -213
  98. package/src/index.ts +0 -78
  99. package/src/repo-store/index.ts +0 -15
  100. package/src/repo-store/store.test.ts +0 -1169
  101. package/src/repo-store/store.ts +0 -428
  102. package/src/repo-store/types.ts +0 -253
  103. package/src/session-service.test.ts +0 -895
  104. package/src/session-service.ts +0 -464
  105. package/src/skill-kind.test.ts +0 -599
  106. package/src/skill-kind.ts +0 -350
  107. package/src/ws/index.ts +0 -18
  108. package/src/ws/sidecar-events.test.ts +0 -96
  109. package/src/ws/sidecar-events.ts +0 -231
  110. package/src/ws/sidecar-handler.test.ts +0 -2217
  111. package/src/ws/sidecar-handler.ts +0 -1574
  112. package/tsconfig.json +0 -4
  113. 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[]>;