@intx/hub-sessions 0.2.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 (73) hide show
  1. package/README.md +3 -5
  2. package/dist/agent-repo.d.ts +9 -5
  3. package/dist/agent-repo.js +2 -2
  4. package/dist/agent-state-kind.js +4 -0
  5. package/dist/asset-service.d.ts +1 -20
  6. package/dist/asset-service.js +9 -91
  7. package/dist/committed-source-tree.d.ts +10 -0
  8. package/dist/committed-source-tree.js +35 -0
  9. package/dist/credential-push.d.ts +7 -6
  10. package/dist/credential-push.js +42 -18
  11. package/dist/event-collector-registry.d.ts +1 -1
  12. package/dist/event-collector-registry.js +4 -4
  13. package/dist/event-collector.d.ts +1 -1
  14. package/dist/event-collector.js +10 -2
  15. package/dist/hub-session-lookups.d.ts +125 -7
  16. package/dist/hub-session-lookups.js +539 -80
  17. package/dist/hub-session-orchestrator.js +14 -49
  18. package/dist/index.d.ts +17 -8
  19. package/dist/index.js +14 -6
  20. package/dist/repo-store/index.d.ts +1 -1
  21. package/dist/repo-store/store.d.ts +1 -1
  22. package/dist/repo-store/store.js +138 -1
  23. package/dist/repo-store/subscribe-kind.d.ts +6 -3
  24. package/dist/repo-store/subscribe-kind.js +42 -77
  25. package/dist/repo-store/types.d.ts +94 -6
  26. package/dist/session-service.d.ts +277 -96
  27. package/dist/session-service.js +741 -547
  28. package/dist/sidecar-allocation/contracts.d.ts +78 -0
  29. package/dist/sidecar-allocation/contracts.js +21 -0
  30. package/dist/sidecar-allocation/index.d.ts +4 -0
  31. package/dist/sidecar-allocation/index.js +3 -0
  32. package/dist/sidecar-allocation/placement-policy.d.ts +11 -0
  33. package/dist/sidecar-allocation/placement-policy.js +21 -0
  34. package/dist/sidecar-allocation/plugin-registry.d.ts +11 -0
  35. package/dist/sidecar-allocation/plugin-registry.js +37 -0
  36. package/dist/sidecar-allocation/reconciler.d.ts +42 -0
  37. package/dist/sidecar-allocation/reconciler.js +431 -0
  38. package/dist/skill-kind.js +4 -0
  39. package/dist/substrate.d.ts +3 -3
  40. package/dist/substrate.js +1 -1
  41. package/dist/workflow-allocation-service.d.ts +58 -0
  42. package/dist/workflow-allocation-service.js +239 -0
  43. package/dist/workflow-closure-resolution.d.ts +106 -0
  44. package/dist/workflow-closure-resolution.js +123 -0
  45. package/dist/workflow-definition-ensure.d.ts +24 -0
  46. package/dist/workflow-definition-ensure.js +75 -0
  47. package/dist/workflow-dispatch-service.d.ts +40 -0
  48. package/dist/workflow-dispatch-service.js +146 -0
  49. package/dist/workflow-dispatch-settlement.d.ts +29 -0
  50. package/dist/workflow-dispatch-settlement.js +140 -0
  51. package/dist/workflow-kind.d.ts +17 -1
  52. package/dist/workflow-kind.js +127 -80
  53. package/dist/workflow-probe-gate.d.ts +214 -0
  54. package/dist/workflow-probe-gate.js +207 -0
  55. package/dist/workflow-run-kind.d.ts +128 -14
  56. package/dist/workflow-run-kind.js +353 -83
  57. package/dist/workflow-run-reader.d.ts +1 -1
  58. package/dist/workflow-run-reader.js +3 -7
  59. package/dist/workflow-run-restore.d.ts +15 -0
  60. package/dist/workflow-run-restore.js +26 -0
  61. package/dist/workflow-source-closure.d.ts +35 -0
  62. package/dist/workflow-source-closure.js +342 -0
  63. package/dist/ws/index.d.ts +3 -3
  64. package/dist/ws/index.js +1 -1
  65. package/dist/ws/sidecar-events.d.ts +100 -12
  66. package/dist/ws/sidecar-events.js +2 -0
  67. package/dist/ws/sidecar-handler.d.ts +128 -7
  68. package/dist/ws/sidecar-handler.js +1069 -135
  69. package/dist/ws/sidecar-token-authenticator.d.ts +3 -1
  70. package/dist/ws/sidecar-token-authenticator.js +64 -7
  71. package/package.json +14 -13
  72. package/dist/available-skills-stanza.d.ts +0 -21
  73. package/dist/available-skills-stanza.js +0 -32
package/README.md CHANGED
@@ -95,11 +95,9 @@ for the authoritative type shapes.
95
95
  ### Asset service
96
96
 
97
97
  - `createAssetService` / `AssetService`, `AssetServiceError`,
98
- `DEFAULT_ASSET_REF`, and the supporting types `Asset`, `AgentAsset`,
99
- `AgentAssetWithAsset`, `AccessMode`, `CreateAssetParams`,
100
- `PopulateAssetParams`, `AttachAssetParams`,
101
- `AssetServiceErrorReason`, `ReadAssetBlobParams`,
102
- `ListAssetBlobsParams`.
98
+ `DEFAULT_ASSET_REF`, and the supporting types `Asset`,
99
+ `CreateAssetParams`, `PopulateAssetParams`, `AssetServiceErrorReason`,
100
+ `ReadAssetBlobParams`, `ListAssetBlobsParams`.
103
101
 
104
102
  ### Skills stanza and credential push
105
103
 
@@ -1,6 +1,6 @@
1
- import type { GCPolicy } from "@intx/storage-isogit";
1
+ import type { GCPolicy } from "@intx/storage-isogit/node";
2
2
  import type { ToolPackageManifest } from "@intx/types/tool-packages";
3
- import type { RepoId, RepoStore } from "./repo-store/index.js";
3
+ import type { NewlyTerminalRun, RepoId, RepoStore } from "./repo-store/index.js";
4
4
  export type DeployContent = {
5
5
  systemPrompt: string;
6
6
  /**
@@ -44,7 +44,7 @@ export type AgentRepoStore = {
44
44
  * tree.
45
45
  *
46
46
  * `repoId.kind` must be `"agent-state"`. The `repoId.id` is used as
47
- * the agent address internally. Stamps an
47
+ * the run address internally. Stamps an
48
48
  * `AgentStateSidecarPrincipal` so the agent-state kind handler
49
49
  * authorizes the receivePack as a per-agent sidecar write.
50
50
  */
@@ -55,13 +55,17 @@ export type AgentRepoStore = {
55
55
  * tree.
56
56
  *
57
57
  * `repoId.kind` must be `"workflow-run"`. Stamps a
58
- * `WorkflowRunSupervisorPrincipal` whose `deploymentId` equals
58
+ * `WorkflowRunSupervisorPrincipal` whose `anchorRunId` equals
59
59
  * `repoId.id`; the workflow-run kind handler denies sidecar-kind
60
60
  * writes by design, so the supervisor branch is the correct path for
61
61
  * sidecar-originated run-event commits (the supervisor lives on the
62
62
  * sidecar and is the actual signer of the run events).
63
+ *
64
+ * Returns the runs the pack drove terminal, surfaced from the
65
+ * substrate's per-commit validation so a caller can flip the run's
66
+ * DB state without re-deriving terminal-ness.
63
67
  */
64
- receiveWorkflowRunPack(repoId: RepoId, pack: Uint8Array, ref: string, commitSha: string): Promise<void>;
68
+ receiveWorkflowRunPack(repoId: RepoId, pack: Uint8Array, ref: string, commitSha: string): Promise<NewlyTerminalRun[]>;
65
69
  /** Resolve the current deploy ref SHA, or null if no deploy exists. */
66
70
  getDeployRef(agentId: string): Promise<string | null>;
67
71
  /** Raw 32-byte Ed25519 public key used to sign deploy commits. */
@@ -93,10 +93,10 @@ export function createAgentRepoStore(config) {
93
93
  }
94
94
  const principal = {
95
95
  kind: "supervisor",
96
- deploymentId: incomingRepoId.id,
96
+ anchorRunId: incomingRepoId.id,
97
97
  };
98
98
  const expectedOldSha = await store.resolveRef(hub, incomingRepoId, ref);
99
- await store.receivePack(principal, incomingRepoId, ref, pack, commitSha, expectedOldSha);
99
+ return store.receivePack(principal, incomingRepoId, ref, pack, commitSha, expectedOldSha);
100
100
  },
101
101
  async getDeployRef(agentId) {
102
102
  return store.resolveRef(hub, repoId(agentId), AGENT_STATE_DEPLOY_REF);
@@ -178,6 +178,10 @@ export const agentStateAuthorize = (principal, repoId, ref, action) => {
178
178
  reason: `authz verdict denied for ${expectedResource} ${expectedGrantVerb}`,
179
179
  };
180
180
  }
181
+ // Fail closed on any kind not handled above. The tenant-level
182
+ // `workflow` principal kind (`@intx/types` principalKinds) is a
183
+ // grant owner, not an agent-state bearer, and never carries an
184
+ // agent-state repo push here -- so it is intentionally left denied.
181
185
  return {
182
186
  allowed: false,
183
187
  reason: `unknown principal kind: ${principal.kind}`,
@@ -11,18 +11,6 @@ export type Asset = {
11
11
  createdAt: Date;
12
12
  updatedAt: Date;
13
13
  };
14
- export type AccessMode = "read-only" | "read-write";
15
- export type AgentAsset = {
16
- id: string;
17
- agentId: string;
18
- assetId: string;
19
- ref: string;
20
- accessMode: AccessMode;
21
- createdAt: Date;
22
- };
23
- export type AgentAssetWithAsset = AgentAsset & {
24
- asset: Pick<Asset, "id" | "tenantId" | "kind" | "name" | "displayName">;
25
- };
26
14
  export type CreateAssetParams = {
27
15
  tenantId: string;
28
16
  /** Accepted kinds: "skill", "package-registry", "workflow".
@@ -48,19 +36,11 @@ export type PopulateAssetParams = {
48
36
  * (e.g. skillAuthorize only permits `kind: "hub"` writes). */
49
37
  principal: Principal;
50
38
  };
51
- export type AttachAssetParams = {
52
- agentId: string;
53
- assetId: string;
54
- ref: string;
55
- accessMode?: AccessMode;
56
- };
57
39
  export interface AssetService {
58
40
  createAsset(params: CreateAssetParams): Promise<Asset>;
59
41
  populateAsset(params: PopulateAssetParams): Promise<{
60
42
  commitSha: string;
61
43
  }>;
62
- attachAsset(params: AttachAssetParams): Promise<AgentAsset>;
63
- listAgentAssets(agentId: string): Promise<AgentAssetWithAsset[]>;
64
44
  /**
65
45
  * In-process blob read. Resolves the asset's row, then reads the blob
66
46
  * at `path` from the commit pointed to by `ref` (defaults to
@@ -99,6 +79,7 @@ export declare const DEFAULT_ASSET_REF = "refs/heads/main";
99
79
  /** Discriminator for AssetServiceError variants. Lets callers branch
100
80
  * without instanceof gymnastics across the different error subclasses. */
101
81
  export type AssetServiceErrorReason = "unsupported_kind" | "duplicate_asset" | "duplicate_attachment" | "invalid_name" | "invalid_reference" | "name_reserved" | "not_found" | "path_violation";
82
+ export declare const ASSET_NAME_PATTERN: RegExp;
102
83
  export declare class AssetServiceError extends Error {
103
84
  readonly reason: AssetServiceErrorReason;
104
85
  constructor(reason: AssetServiceErrorReason, message: string, cause?: unknown);
@@ -1,4 +1,4 @@
1
- // In-process service for creating and attaching skill-asset repos.
1
+ // In-process service for creating and populating skill-asset repos.
2
2
  //
3
3
  // Three responsibilities are layered here, mirroring the substrate's
4
4
  // own layering (DB row, repo bookkeeping, content validation):
@@ -8,19 +8,15 @@
8
8
  // populateAsset drives RepoStore.writeTree, which runs the kind
9
9
  // handler's validatePush before advancing the ref.
10
10
  // Content rejections surface as AssetValidationError.
11
- // attachAsset inserts an agent_asset row, surfacing the
12
- // (agentId, assetId) uniqueness violation (which
13
- // prevents the same asset being attached to one
14
- // agent twice) as AssetAttachError.
15
11
  //
16
12
  // The factory is closure-based to match createAgentRepoStore and
17
13
  // createRepoStore. There is no class because there is no per-instance
18
14
  // mutable state — every method is a pure function over the deps.
19
15
  import fs from "node:fs";
20
- import { asc, eq } from "drizzle-orm";
16
+ import { eq } from "drizzle-orm";
21
17
  import git from "isomorphic-git";
22
- import { pgErrorCode, PG_UNIQUE_VIOLATION, PG_FOREIGN_KEY_VIOLATION, } from "@intx/db";
23
- import { agentAsset as agentAssetTable, asset as assetTable, } from "@intx/db/schema";
18
+ import { pgErrorCode, PG_UNIQUE_VIOLATION } from "@intx/db";
19
+ import { asset as assetTable } from "@intx/db/schema";
24
20
  import { generateId } from "@intx/hub-common";
25
21
  import { getLogger } from "@intx/log";
26
22
  const logger = getLogger(["hub-sessions", "asset-service"]);
@@ -36,8 +32,11 @@ export const DEFAULT_ASSET_REF = "refs/heads/main";
36
32
  // character set; validate at the createAsset boundary so a bad name
37
33
  // fails at creation time rather than at materialization time. Names
38
34
  // must be lowercase-kebab: lowercase letters, digits, hyphens, with
39
- // no leading or trailing hyphen.
40
- const ASSET_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
35
+ // no leading or trailing hyphen. Exported so a caller deriving a name
36
+ // (the agent-fold materializer) asserts the shape at its own boundary
37
+ // rather than discovering a violation three layers in as a generic
38
+ // `invalid_name`.
39
+ export const ASSET_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
41
40
  export class AssetServiceError extends Error {
42
41
  reason;
43
42
  constructor(reason, message, cause) {
@@ -46,9 +45,6 @@ export class AssetServiceError extends Error {
46
45
  this.reason = reason;
47
46
  }
48
47
  }
49
- function isAccessMode(value) {
50
- return value === "read-only" || value === "read-write";
51
- }
52
48
  /**
53
49
  * Reject malformed `dir` arguments at the `listAssetBlobs` boundary
54
50
  * before any tree walk begins. The empty string is the documented
@@ -112,19 +108,6 @@ function rowToAsset(row) {
112
108
  updatedAt: row.updatedAt,
113
109
  };
114
110
  }
115
- function rowToAgentAsset(row) {
116
- if (!isAccessMode(row.accessMode)) {
117
- throw new Error(`agent_asset row ${row.id} has unknown accessMode ${JSON.stringify(row.accessMode)}`);
118
- }
119
- return {
120
- id: row.id,
121
- agentId: row.agentId,
122
- assetId: row.assetId,
123
- ref: row.ref,
124
- accessMode: row.accessMode,
125
- createdAt: row.createdAt,
126
- };
127
- }
128
111
  export function createAssetService(deps) {
129
112
  const { db, repoStore } = deps;
130
113
  const reservedPackageRegistryNames = deps.reservedPackageRegistryNames ?? new Set();
@@ -216,69 +199,6 @@ export function createAssetService(deps) {
216
199
  throw err;
217
200
  }
218
201
  }
219
- async function attachAsset(params) {
220
- const id = generateId("agentAsset");
221
- const accessMode = params.accessMode ?? "read-only";
222
- const insertRow = {
223
- id,
224
- agentId: params.agentId,
225
- assetId: params.assetId,
226
- ref: params.ref,
227
- accessMode,
228
- createdAt: new Date(),
229
- };
230
- let inserted;
231
- try {
232
- const rows = await db
233
- .insert(agentAssetTable)
234
- .values(insertRow)
235
- .returning();
236
- const row = rows[0];
237
- if (row === undefined) {
238
- throw new Error("insert into agent_asset returned no rows");
239
- }
240
- inserted = row;
241
- }
242
- catch (err) {
243
- if (pgErrorCode(err) === PG_UNIQUE_VIOLATION) {
244
- throw new AssetServiceError("duplicate_attachment", `agent_asset (agentId=${params.agentId}, assetId=${params.assetId}) already attached`, err);
245
- }
246
- if (pgErrorCode(err) === PG_FOREIGN_KEY_VIOLATION) {
247
- throw new AssetServiceError("invalid_reference", `agent_asset (agentId=${params.agentId}, assetId=${params.assetId}) references a missing agent or asset`, err);
248
- }
249
- throw err;
250
- }
251
- return rowToAgentAsset(inserted);
252
- }
253
- async function listAgentAssets(agentId) {
254
- // Order by (createdAt, id) so the row sequence is stable across reads
255
- // — the available_skills stanza and pack fan-out both depend on a
256
- // deterministic order, and Postgres does not guarantee one without
257
- // an explicit orderBy.
258
- const rows = await db
259
- .select({
260
- agentAsset: agentAssetTable,
261
- asset: assetTable,
262
- })
263
- .from(agentAssetTable)
264
- .innerJoin(assetTable, eq(agentAssetTable.assetId, assetTable.id))
265
- .where(eq(agentAssetTable.agentId, agentId))
266
- .orderBy(asc(agentAssetTable.createdAt), asc(agentAssetTable.id));
267
- return rows.map((row) => {
268
- const aa = rowToAgentAsset(row.agentAsset);
269
- const a = rowToAsset(row.asset);
270
- return {
271
- ...aa,
272
- asset: {
273
- id: a.id,
274
- tenantId: a.tenantId,
275
- kind: a.kind,
276
- name: a.name,
277
- displayName: a.displayName,
278
- },
279
- };
280
- });
281
- }
282
202
  async function resolveAssetRowOrThrow(assetId, label) {
283
203
  const row = await db.query.asset.findFirst({
284
204
  where: eq(assetTable.id, assetId),
@@ -341,8 +261,6 @@ export function createAssetService(deps) {
341
261
  return {
342
262
  createAsset,
343
263
  populateAsset,
344
- attachAsset,
345
- listAgentAssets,
346
264
  readAssetBlob,
347
265
  listAssetBlobs,
348
266
  };
@@ -0,0 +1,10 @@
1
+ import type { CommittedReads } from "./repo-store/types.js";
2
+ import type { SourceTreeReads } from "./workflow-source-closure.js";
3
+ /**
4
+ * Wrap `reads` so `readBlob(path)` resolves a repo-relative POSIX path to its
5
+ * blob: list the path's parent directory, match the final segment as a `blob`
6
+ * entry, then read by its object id. `listDir` and `treeOid` pass straight
7
+ * through -- both handles already speak repo-relative paths and return the same
8
+ * shapes.
9
+ */
10
+ export declare function committedReadsToSourceTree(reads: CommittedReads): SourceTreeReads;
@@ -0,0 +1,35 @@
1
+ // Adapts a repo-store `CommittedReads` handle (reads pinned to a commit,
2
+ // addressed by object id) to the `SourceTreeReads` shape the source-closure
3
+ // resolver consumes (reads addressed by repo-relative path). This bridge lives
4
+ // at the caller layer, not inside the resolver: the resolver stays agnostic of
5
+ // where its tree bytes come from (a test fake, this store, a future one), and
6
+ // the repo-store stays unaware of the closure interface. Only a layer that
7
+ // composes both types -- the install/deploy glue's caller -- owns the join.
8
+ /**
9
+ * Wrap `reads` so `readBlob(path)` resolves a repo-relative POSIX path to its
10
+ * blob: list the path's parent directory, match the final segment as a `blob`
11
+ * entry, then read by its object id. `listDir` and `treeOid` pass straight
12
+ * through -- both handles already speak repo-relative paths and return the same
13
+ * shapes.
14
+ */
15
+ export function committedReadsToSourceTree(reads) {
16
+ return {
17
+ async readBlob(blobPath) {
18
+ // The parent of a top-level path (no slash) is the root tree, which
19
+ // `CommittedReads.listDir` addresses with the empty string.
20
+ const slash = blobPath.lastIndexOf("/");
21
+ const parentDir = slash === -1 ? "" : blobPath.slice(0, slash);
22
+ const name = slash === -1 ? blobPath : blobPath.slice(slash + 1);
23
+ const entries = await reads.listDir(parentDir);
24
+ // Match on the blob type so a path naming a directory or a gitlink fails
25
+ // loud here rather than handing a tree/commit oid to `readBlobByOid`.
26
+ const entry = entries.find((e) => e.name === name && e.type === "blob");
27
+ if (entry === undefined) {
28
+ throw new Error(`committedReadsToSourceTree: no blob at ${JSON.stringify(blobPath)}`);
29
+ }
30
+ return reads.readBlobByOid(entry.oid);
31
+ },
32
+ listDir: (dir) => reads.listDir(dir),
33
+ treeOid: (dir) => reads.treeOid(dir),
34
+ };
35
+ }
@@ -1,9 +1,10 @@
1
1
  import type { DB } from "@intx/db";
2
+ import type { CredentialCipher } from "@intx/types";
2
3
  import type { SidecarRouter } from "./ws/sidecar-handler.js";
3
4
  /**
4
5
  * Re-resolve a single running instance's inference sources from the catalog
5
- * (the agent's model requirements plus the invoker preferences persisted on
6
- * the instance) and push the ordered list to its sidecar. The head of the
6
+ * (the definition's model requirements plus the invoker preferences persisted
7
+ * on the instance) and push the ordered list to its sidecar. The head of the
7
8
  * catalog-priority-ordered list is the active default; the tail is the
8
9
  * failover chain.
9
10
  *
@@ -12,21 +13,21 @@ import type { SidecarRouter } from "./ws/sidecar-handler.js";
12
13
  */
13
14
  export declare function pushInstanceSourceUpdate(db: DB["db"], sidecarRouter: Pick<SidecarRouter, "sendSourcesUpdate">, instance: {
14
15
  address: string;
15
- agentId: string;
16
+ definitionId: string;
16
17
  tenantId: string;
17
18
  modelPreferences: unknown;
18
- }): Promise<void>;
19
+ }, credentialCipher?: CredentialCipher): Promise<void>;
19
20
  /**
20
21
  * After a credential secret is rotated, re-resolve every running instance in
21
22
  * the tenant against the catalog and push the updates. A rotated secret flows
22
23
  * through because resolution dereferences the provider's credential reference
23
24
  * to the current secret.
24
25
  */
25
- export declare function pushSourceUpdates(db: DB["db"], sidecarRouter: SidecarRouter, tenantId: string): Promise<void>;
26
+ export declare function pushSourceUpdates(db: DB["db"], sidecarRouter: SidecarRouter, tenantId: string, credentialCipher?: CredentialCipher): Promise<void>;
26
27
  /**
27
28
  * After a catalog edit in a tenant, re-resolve and push to every running
28
29
  * instance in that tenant AND its descendants. Descendants inherit the
29
30
  * edited tenant's catalog, so a change there (a disabled provider, a new
30
31
  * offering, a price update) alters their resolved sources too.
31
32
  */
32
- export declare function pushSourceUpdatesSubtree(db: DB["db"], sidecarRouter: SidecarRouter, tenantId: string): Promise<void>;
33
+ export declare function pushSourceUpdatesSubtree(db: DB["db"], sidecarRouter: SidecarRouter, tenantId: string, credentialCipher?: CredentialCipher): Promise<void>;
@@ -2,24 +2,25 @@
2
2
  // the catalog and pushing the update to its sidecar.
3
3
  //
4
4
  // Used after a credential secret rotation (a model provider's credential
5
- // changes the resolved source's apiKey) and on sidecar reconnect.
6
- import { eq, and, inArray } from "drizzle-orm";
5
+ // changes the resolved source's apiKey) and after a catalog edit in a tenant
6
+ // or its subtree.
7
+ import { eq, and, inArray, isNull, isNotNull } from "drizzle-orm";
7
8
  import { getLogger } from "@intx/log";
8
- import { agentInstance } from "@intx/db/schema";
9
+ import { workflowRun } from "@intx/db/schema";
9
10
  import { resolveInstanceModelSources, getDescendantTenants } from "@intx/db";
10
11
  const log = getLogger(["hub", "credentials"]);
11
12
  /**
12
13
  * Re-resolve a single running instance's inference sources from the catalog
13
- * (the agent's model requirements plus the invoker preferences persisted on
14
- * the instance) and push the ordered list to its sidecar. The head of the
14
+ * (the definition's model requirements plus the invoker preferences persisted
15
+ * on the instance) and push the ordered list to its sidecar. The head of the
15
16
  * catalog-priority-ordered list is the active default; the tail is the
16
17
  * failover chain.
17
18
  *
18
19
  * No-op when the instance resolves to no launchable source — the resolver's
19
20
  * own logger is the signal for why.
20
21
  */
21
- export async function pushInstanceSourceUpdate(db, sidecarRouter, instance) {
22
- const resolution = await resolveInstanceModelSources(db, instance.tenantId, instance);
22
+ export async function pushInstanceSourceUpdate(db, sidecarRouter, instance, credentialCipher) {
23
+ const resolution = await resolveInstanceModelSources(db, instance.tenantId, instance, credentialCipher);
23
24
  if (!resolution.ok)
24
25
  return;
25
26
  const [head] = resolution.sources;
@@ -29,11 +30,15 @@ export async function pushInstanceSourceUpdate(db, sidecarRouter, instance) {
29
30
  }
30
31
  /**
31
32
  * Re-resolve every running instance in the given tenants against the catalog
32
- * and push the updates to sidecars. Each instance re-resolves from its own
33
- * tenant's context (its ancestor chain), so the rotated/edited upstream entry
34
- * flows through. Errors are logged per-instance but do not propagate.
33
+ * and push the updates to sidecars. The running instances are the folded runs
34
+ * a launch produces: born running, with a routing address and no deployment.
35
+ * Deployment-anchor runs (which own a deployment id and a workflow-derived
36
+ * address) and address-less child runs route via the deployment, not this
37
+ * per-instance push, so they are excluded. Each instance re-resolves from its
38
+ * own tenant's context (its ancestor chain), so the rotated/edited upstream
39
+ * entry flows through. Errors are logged per-instance but do not propagate.
35
40
  */
36
- async function pushSourceUpdatesToTenants(db, sidecarRouter, tenantIds) {
41
+ async function pushSourceUpdatesToTenants(db, sidecarRouter, tenantIds, credentialCipher) {
37
42
  if (tenantIds.length === 0)
38
43
  return;
39
44
  // Callers fire this without awaiting, so it must never reject: a failure to
@@ -41,12 +46,31 @@ async function pushSourceUpdatesToTenants(db, sidecarRouter, tenantIds) {
41
46
  // rejection. The push is best effort — the next mutation or a sidecar
42
47
  // reconnect re-resolves sources.
43
48
  try {
44
- const instances = await db.query.agentInstance.findMany({
45
- where: and(inArray(agentInstance.tenantId, tenantIds), eq(agentInstance.status, "running")),
49
+ // The instance-shaped runs: running, addressable, and not anchored on a
50
+ // deployment. `anchorRunId IS NULL` excludes the deployment-anchor runs
51
+ // (which set it to their own id), so no deployment-anchor run address can
52
+ // reach the address-targeted push below. Mirrors the /me/workflows/runs and
53
+ // tenant run-list predicate.
54
+ const instances = await db.query.workflowRun.findMany({
55
+ where: and(inArray(workflowRun.tenantId, tenantIds), eq(workflowRun.status, "running"), isNull(workflowRun.anchorRunId), isNotNull(workflowRun.address)),
46
56
  });
47
57
  if (instances.length === 0)
48
58
  return;
49
- const results = await Promise.allSettled(instances.map((instance) => pushInstanceSourceUpdate(db, sidecarRouter, instance)));
59
+ const results = await Promise.allSettled(instances.map(async (instance) => {
60
+ // The isNotNull(address) filter guarantees a value; a null here is a
61
+ // broken invariant. The callback is async, so this throw becomes a
62
+ // rejected promise captured per-instance by allSettled and logged
63
+ // below -- one bad row is surfaced, not fatal to the whole batch.
64
+ if (instance.address === null) {
65
+ throw new Error(`running run ${instance.id} matched the non-null-address filter but has a null address`);
66
+ }
67
+ return pushInstanceSourceUpdate(db, sidecarRouter, {
68
+ address: instance.address,
69
+ definitionId: instance.definitionId,
70
+ tenantId: instance.tenantId,
71
+ modelPreferences: instance.modelPreferences,
72
+ }, credentialCipher);
73
+ }));
50
74
  for (const result of results) {
51
75
  if (result.status === "rejected") {
52
76
  log.warn `Failed to push source update: ${String(result.reason)}`;
@@ -63,8 +87,8 @@ async function pushSourceUpdatesToTenants(db, sidecarRouter, tenantIds) {
63
87
  * through because resolution dereferences the provider's credential reference
64
88
  * to the current secret.
65
89
  */
66
- export async function pushSourceUpdates(db, sidecarRouter, tenantId) {
67
- await pushSourceUpdatesToTenants(db, sidecarRouter, [tenantId]);
90
+ export async function pushSourceUpdates(db, sidecarRouter, tenantId, credentialCipher) {
91
+ await pushSourceUpdatesToTenants(db, sidecarRouter, [tenantId], credentialCipher);
68
92
  }
69
93
  /**
70
94
  * After a catalog edit in a tenant, re-resolve and push to every running
@@ -72,7 +96,7 @@ export async function pushSourceUpdates(db, sidecarRouter, tenantId) {
72
96
  * edited tenant's catalog, so a change there (a disabled provider, a new
73
97
  * offering, a price update) alters their resolved sources too.
74
98
  */
75
- export async function pushSourceUpdatesSubtree(db, sidecarRouter, tenantId) {
99
+ export async function pushSourceUpdatesSubtree(db, sidecarRouter, tenantId, credentialCipher) {
76
100
  let tenants;
77
101
  try {
78
102
  tenants = await getDescendantTenants(db, tenantId);
@@ -81,5 +105,5 @@ export async function pushSourceUpdatesSubtree(db, sidecarRouter, tenantId) {
81
105
  log.warn `Failed to enumerate descendants for source push: ${String(err)}`;
82
106
  return;
83
107
  }
84
- await pushSourceUpdatesToTenants(db, sidecarRouter, tenants);
108
+ await pushSourceUpdatesToTenants(db, sidecarRouter, tenants, credentialCipher);
85
109
  }
@@ -3,7 +3,7 @@ import type { InferenceEvent } from "@intx/types/runtime";
3
3
  import type { SessionStatus } from "@intx/types";
4
4
  import { type TurnFinalized } from "./event-collector.js";
5
5
  export type EventCollectorRegistry = {
6
- create(agentAddress: string, tenantId: string, sessionId: string, instanceId: string): void;
6
+ create(agentAddress: string, tenantId: string, sessionId: string, runId: string): void;
7
7
  dispatch(agentAddress: string, event: InferenceEvent): void;
8
8
  abandon(agentAddress: string): void;
9
9
  has(agentAddress: string): boolean;
@@ -1,8 +1,8 @@
1
- // Registry of active event collectors, keyed by agent address.
1
+ // Registry of active event collectors, keyed by run address.
2
2
  //
3
3
  // The hub creates a collector when an instance starts and removes it when the
4
4
  // instance ends or the sidecar disconnects. The hub session orchestrator's
5
- // `agent.event` listener looks up the collector by agent address and
5
+ // `agent.event` listener looks up the collector by run address and
6
6
  // dispatches the event.
7
7
  import { getLogger } from "@intx/log";
8
8
  import { createEventCollector, } from "./event-collector.js";
@@ -33,7 +33,7 @@ export function createEventCollectorRegistry(config) {
33
33
  const { db, onTurnFinalized } = config;
34
34
  const collectors = new Map();
35
35
  const statuses = new Map();
36
- function create(agentAddress, tenantId, sessionId, instanceId) {
36
+ function create(agentAddress, tenantId, sessionId, runId) {
37
37
  if (collectors.has(agentAddress)) {
38
38
  log.warn `Collector already exists for ${agentAddress}, replacing`;
39
39
  abandon(agentAddress);
@@ -41,7 +41,7 @@ export function createEventCollectorRegistry(config) {
41
41
  const collector = createEventCollector({
42
42
  db,
43
43
  sessionId,
44
- instanceId,
44
+ runId,
45
45
  tenantId,
46
46
  ...(onTurnFinalized
47
47
  ? {
@@ -32,7 +32,7 @@ export type EventCollector = {
32
32
  export type EventCollectorConfig = {
33
33
  db: DB["db"];
34
34
  sessionId: string;
35
- instanceId: string;
35
+ runId: string;
36
36
  tenantId: string;
37
37
  onTurnFinalized?: (turn: TurnFinalized) => void;
38
38
  };
@@ -10,7 +10,7 @@ import { parseTurnPartType } from "@intx/db";
10
10
  import { generateId } from "@intx/hub-common";
11
11
  const log = getLogger(["hub", "event-collector"]);
12
12
  export function createEventCollector(config) {
13
- const { db, sessionId, instanceId, tenantId, onTurnFinalized } = config;
13
+ const { db, sessionId, runId, tenantId, onTurnFinalized } = config;
14
14
  // Current inference turn being accumulated. A new turn is created on each
15
15
  // inference.start. Finalized on connector.reply, reactor.done,
16
16
  // reactor.error (fatal), or abandon. Null when no turn is active.
@@ -173,7 +173,7 @@ export function createEventCollector(config) {
173
173
  await db.insert(inferenceTurn).values({
174
174
  id: currentTurnId,
175
175
  sessionId,
176
- instanceId,
176
+ runId,
177
177
  tenantId,
178
178
  model,
179
179
  status: "running",
@@ -208,6 +208,14 @@ export function createEventCollector(config) {
208
208
  // declines differently from regular assistant output.
209
209
  await insertPart("refusal", block.reason, null);
210
210
  break;
211
+ case "safety_rating":
212
+ // Structured safety signals (e.g. Gemini
213
+ // promptFeedback.blockReason). Persist the reason under a
214
+ // dedicated part kind so a blocked turn is not empty in
215
+ // the audit trail. Content is the provider-native reason
216
+ // string (observed: PROHIBITED_CONTENT).
217
+ await insertPart("safety_rating", block.blockReason, null);
218
+ break;
211
219
  case "tool_call":
212
220
  callNames.set(block.id, block.name);
213
221
  callArgs.set(block.id, block.arguments);