@codemation/host 0.1.0 → 0.1.3

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 (70) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/{AppConfigFactory-Ciz9YKWx.js → AppConfigFactory-ByT1D8dM.js} +253 -15
  3. package/dist/{AppConfigFactory-Ciz9YKWx.js.map → AppConfigFactory-ByT1D8dM.js.map} +1 -1
  4. package/dist/{AppConfigFactory-CiBPHleh.d.ts → AppConfigFactory-_fqSok1J.d.ts} +6565 -177
  5. package/dist/{AppContainerFactory-DH88oxpg.js → AppContainerFactory-BRU02PTm.js} +838 -145
  6. package/dist/AppContainerFactory-BRU02PTm.js.map +1 -0
  7. package/dist/{CodemationConfig-DfK1KLvO.d.ts → CodemationConfig-CNfytKR6.d.ts} +2 -2
  8. package/dist/{CodemationConfigNormalizer-BKgIOeLm.d.ts → CodemationConfigNormalizer-BuKWVNEq.d.ts} +2 -2
  9. package/dist/{CodemationConsumerConfigLoader-bdhJsBKt.d.ts → CodemationConsumerConfigLoader-Mv4cywWu.d.ts} +2 -2
  10. package/dist/{CodemationPluginListMerger-BFZeO0WG.d.ts → CodemationPluginListMerger-BD5mR6gK.d.ts} +11 -5
  11. package/dist/{CredentialServices-aKIwHEhf.d.ts → CredentialServices-BQsEtctT.d.ts} +8 -7
  12. package/dist/{CredentialServices-DNb3CZwW.js → CredentialServices-xVxVA9Tq.js} +60 -114
  13. package/dist/CredentialServices-xVxVA9Tq.js.map +1 -0
  14. package/dist/{PublicFrontendBootstrapFactory-6ahaU0XM.d.ts → PublicFrontendBootstrapFactory-kTyAJdHI.d.ts} +2 -2
  15. package/dist/consumer.d.ts +4 -4
  16. package/dist/credentials.d.ts +3 -3
  17. package/dist/credentials.js +1 -1
  18. package/dist/devServerSidecar.d.ts +1 -1
  19. package/dist/{index-BYbzmUwS.d.ts → index-CX752QE9.d.ts} +68 -4
  20. package/dist/index.d.ts +10 -10
  21. package/dist/index.js +5 -5
  22. package/dist/nextServer.d.ts +20 -15
  23. package/dist/nextServer.js +35 -58
  24. package/dist/nextServer.js.map +1 -1
  25. package/dist/{persistenceServer-DL8yBGDU.d.ts → persistenceServer-CLY4qtMo.d.ts} +2 -2
  26. package/dist/{persistenceServer-CuAqL_fF.js → persistenceServer-DMvIOGW8.js} +2 -2
  27. package/dist/{persistenceServer-CuAqL_fF.js.map → persistenceServer-DMvIOGW8.js.map} +1 -1
  28. package/dist/persistenceServer.d.ts +5 -5
  29. package/dist/persistenceServer.js +2 -2
  30. package/dist/{server-EbxQft_X.js → server-ChTCEc6R.js} +4 -4
  31. package/dist/{server-EbxQft_X.js.map → server-ChTCEc6R.js.map} +1 -1
  32. package/dist/{server-BceIfIJf.d.ts → server-DwpcwzFb.d.ts} +6 -5
  33. package/dist/server.d.ts +8 -8
  34. package/dist/server.js +5 -5
  35. package/package.json +5 -5
  36. package/prisma/migrations/20260407140000_run_normalized_persistence/migration.sql +327 -0
  37. package/prisma/migrations/20260407193000_rename_run_projection_to_run_slot_projection/migration.sql +10 -0
  38. package/prisma/migrations.sqlite/20260407140000_run_normalized_persistence/migration.sql +326 -0
  39. package/prisma/migrations.sqlite/20260407193000_rename_run_projection_to_run_slot_projection/migration.sql +38 -0
  40. package/prisma/schema.postgresql.prisma +100 -1
  41. package/prisma/schema.sqlite.prisma +101 -1
  42. package/scripts/integration-database-global-setup.mjs +0 -3
  43. package/src/application/mapping/WorkflowDefinitionMapper.ts +95 -56
  44. package/src/application/mapping/WorkflowPolicyUiPresentationFactory.ts +1 -1
  45. package/src/application/queries/GetWorkflowRunDetailQuery.ts +8 -0
  46. package/src/application/queries/GetWorkflowRunDetailQueryHandler.ts +24 -0
  47. package/src/application/queries/WorkflowQueryHandlers.ts +1 -0
  48. package/src/application/runs/WorkflowRunRetentionPruneScheduler.ts +52 -27
  49. package/src/domain/credentials/WorkflowCredentialNodeResolver.ts +113 -158
  50. package/src/domain/runs/WorkflowRunRepository.ts +7 -1
  51. package/src/infrastructure/persistence/InMemoryWorkflowRunRepository.ts +123 -1
  52. package/src/infrastructure/persistence/PrismaMigrationDeployer.ts +226 -6
  53. package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +796 -109
  54. package/src/infrastructure/persistence/generated/prisma-postgresql-client/edge.js +85 -5
  55. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index-browser.js +81 -1
  56. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.d.ts +7107 -237
  57. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.js +85 -5
  58. package/src/infrastructure/persistence/generated/prisma-postgresql-client/package.json +1 -1
  59. package/src/infrastructure/persistence/generated/prisma-postgresql-client/schema.prisma +101 -1
  60. package/src/infrastructure/persistence/generated/prisma-sqlite-client/edge.js +85 -5
  61. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index-browser.js +81 -1
  62. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.d.ts +7104 -242
  63. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.js +85 -5
  64. package/src/infrastructure/persistence/generated/prisma-sqlite-client/package.json +1 -1
  65. package/src/infrastructure/persistence/generated/prisma-sqlite-client/schema.prisma +101 -1
  66. package/src/presentation/http/ApiPaths.ts +4 -0
  67. package/src/presentation/http/hono/registrars/RunHonoApiRouteRegistrar.ts +1 -0
  68. package/src/presentation/http/routeHandlers/RunHttpRouteHandler.ts +13 -0
  69. package/dist/AppContainerFactory-DH88oxpg.js.map +0 -1
  70. package/dist/CredentialServices-DNb3CZwW.js.map +0 -1
@@ -1,12 +1,18 @@
1
- import type {
2
- ChatModelConfig,
3
- NodeDefinition,
4
- ToolConfig,
5
- WorkflowActivationPolicy,
6
- WorkflowDefinition,
1
+ import type { AgentNodeConfig, NodeDefinition, WorkflowActivationPolicy, WorkflowDefinition } from "@codemation/core";
2
+ import {
3
+ AgentConfigInspector,
4
+ AgentConnectionNodeCollector,
5
+ CoreTokens,
6
+ inject,
7
+ injectable,
8
+ type AgentConnectionNodeDescriptor,
7
9
  } from "@codemation/core";
8
- import { AgentConfigInspector, ConnectionNodeIdFactory, CoreTokens, inject, injectable } from "@codemation/core";
9
- import type { WorkflowDto, WorkflowNodeDto, WorkflowSummary } from "../contracts/WorkflowViewContracts";
10
+ import type {
11
+ WorkflowDto,
12
+ WorkflowEdgeDto,
13
+ WorkflowNodeDto,
14
+ WorkflowSummary,
15
+ } from "../contracts/WorkflowViewContracts";
10
16
  import type { DataMapper } from "./DataMapper";
11
17
  import { WorkflowPolicyUiPresentationFactory } from "./WorkflowPolicyUiPresentationFactory";
12
18
 
@@ -55,17 +61,23 @@ export class WorkflowDefinitionMapper implements DataMapper<WorkflowDefinition,
55
61
  return map;
56
62
  }
57
63
 
58
- private agentHasConnectionMetadata(workflow: WorkflowDefinition, agentNodeId: string): boolean {
59
- return (workflow.connections ?? []).some((c) => c.parentNodeId === agentNodeId);
60
- }
61
-
62
64
  private toNodes(workflow: WorkflowDefinition): ReadonlyArray<WorkflowNodeDto> {
63
65
  const connectionChildMeta = this.buildConnectionChildMeta(workflow);
66
+ const materializedConnectionNodeIds = new Set(connectionChildMeta.keys());
64
67
  const nodes: WorkflowNodeDto[] = [];
65
68
  for (const node of workflow.nodes) {
66
69
  const conn = connectionChildMeta.get(node.id);
67
70
  if (conn) {
68
- const role = conn.connectionName === "llm" ? "languageModel" : "tool";
71
+ const parentNode = workflow.nodes.find((n) => n.id === conn.parentNodeId);
72
+ let role: string = conn.connectionName === "llm" ? "languageModel" : "tool";
73
+ if (parentNode && AgentConfigInspector.isAgentNodeConfig(parentNode.config)) {
74
+ const descriptor = AgentConnectionNodeCollector.collect(conn.parentNodeId, parentNode.config).find(
75
+ (d) => d.nodeId === node.id,
76
+ );
77
+ if (descriptor) {
78
+ role = descriptor.role;
79
+ }
80
+ }
69
81
  nodes.push({
70
82
  id: node.id,
71
83
  kind: node.kind,
@@ -89,71 +101,98 @@ export class WorkflowDefinitionMapper implements DataMapper<WorkflowDefinition,
89
101
  retryPolicySummary: this.policyUi.nodeRetrySummary(node.config),
90
102
  hasNodeErrorHandler: this.policyUi.nodeHasErrorHandler(node.config),
91
103
  });
92
- if (AgentConfigInspector.isAgentNodeConfig(node.config) && !this.agentHasConnectionMetadata(workflow, node.id)) {
93
- nodes.push(this.createLanguageModelNode(node, node.config.chatModel));
94
- for (const toolConfig of node.config.tools ?? []) {
95
- nodes.push(this.createToolNode(node, toolConfig));
96
- }
104
+ if (AgentConfigInspector.isAgentNodeConfig(node.config)) {
105
+ this.appendVirtualConnectionNodes(node.id, node.config, materializedConnectionNodeIds, nodes);
97
106
  }
98
107
  }
99
108
  return nodes;
100
109
  }
101
110
 
102
111
  private toEdges(workflow: WorkflowDefinition): WorkflowDto["edges"] {
103
- const edges = [...workflow.edges];
112
+ const connectionChildMeta = this.buildConnectionChildMeta(workflow);
113
+ const materializedConnectionNodeIds = new Set(connectionChildMeta.keys());
114
+ const edges: WorkflowEdgeDto[] = [...workflow.edges];
115
+ const edgeKeys = new Set(edges.map((edge) => this.edgeKey(edge.from.nodeId, edge.to.nodeId, edge.to.input)));
116
+ this.appendMaterializedConnectionEdges(workflow, edgeKeys, edges);
104
117
  for (const node of workflow.nodes) {
105
118
  if (!AgentConfigInspector.isAgentNodeConfig(node.config)) {
106
119
  continue;
107
120
  }
108
- if (this.agentHasConnectionMetadata(workflow, node.id)) {
109
- for (const c of workflow.connections ?? []) {
110
- if (c.parentNodeId !== node.id) {
111
- continue;
112
- }
113
- for (const childId of c.childNodeIds) {
114
- edges.push({
115
- from: { nodeId: node.id, output: "main" },
116
- to: { nodeId: childId, input: "in" },
117
- });
118
- }
121
+ this.appendVirtualConnectionEdges(node.id, node.config, materializedConnectionNodeIds, edgeKeys, edges);
122
+ }
123
+ return edges;
124
+ }
125
+
126
+ private appendMaterializedConnectionEdges(
127
+ workflow: WorkflowDefinition,
128
+ edgeKeys: Set<string>,
129
+ edges: WorkflowEdgeDto[],
130
+ ): void {
131
+ for (const connection of workflow.connections ?? []) {
132
+ for (const childNodeId of connection.childNodeIds) {
133
+ const key = this.edgeKey(connection.parentNodeId, childNodeId, "in");
134
+ if (edgeKeys.has(key)) {
135
+ continue;
119
136
  }
137
+ edges.push({
138
+ from: { nodeId: connection.parentNodeId, output: "main" },
139
+ to: { nodeId: childNodeId, input: "in" },
140
+ });
141
+ edgeKeys.add(key);
142
+ }
143
+ }
144
+ }
145
+
146
+ private appendVirtualConnectionNodes(
147
+ rootAgentNodeId: string,
148
+ agentConfig: AgentNodeConfig<any, any>,
149
+ materializedConnectionNodeIds: ReadonlySet<string>,
150
+ nodes: WorkflowNodeDto[],
151
+ ): void {
152
+ for (const connectionNode of AgentConnectionNodeCollector.collect(rootAgentNodeId, agentConfig)) {
153
+ if (materializedConnectionNodeIds.has(connectionNode.nodeId)) {
154
+ continue;
155
+ }
156
+ nodes.push(this.createConnectionNode(connectionNode));
157
+ }
158
+ }
159
+
160
+ private appendVirtualConnectionEdges(
161
+ rootAgentNodeId: string,
162
+ agentConfig: AgentNodeConfig<any, any>,
163
+ materializedConnectionNodeIds: ReadonlySet<string>,
164
+ edgeKeys: Set<string>,
165
+ edges: WorkflowEdgeDto[],
166
+ ): void {
167
+ for (const connectionNode of AgentConnectionNodeCollector.collect(rootAgentNodeId, agentConfig)) {
168
+ if (materializedConnectionNodeIds.has(connectionNode.nodeId)) {
169
+ continue;
170
+ }
171
+ const key = this.edgeKey(connectionNode.parentNodeId, connectionNode.nodeId, "in");
172
+ if (edgeKeys.has(key)) {
120
173
  continue;
121
174
  }
122
175
  edges.push({
123
- from: { nodeId: node.id, output: "main" },
124
- to: { nodeId: ConnectionNodeIdFactory.languageModelConnectionNodeId(node.id), input: "in" },
176
+ from: { nodeId: connectionNode.parentNodeId, output: "main" },
177
+ to: { nodeId: connectionNode.nodeId, input: "in" },
125
178
  });
126
- for (const toolConfig of node.config.tools ?? []) {
127
- edges.push({
128
- from: { nodeId: node.id, output: "main" },
129
- to: { nodeId: ConnectionNodeIdFactory.toolConnectionNodeId(node.id, toolConfig.name), input: "in" },
130
- });
131
- }
179
+ edgeKeys.add(key);
132
180
  }
133
- return edges;
134
181
  }
135
182
 
136
- private createLanguageModelNode(node: NodeDefinition, chatModel: ChatModelConfig): WorkflowNodeDto {
137
- return {
138
- id: ConnectionNodeIdFactory.languageModelConnectionNodeId(node.id),
139
- kind: "node",
140
- name: chatModel.presentation?.label ?? chatModel.name,
141
- type: chatModel.name,
142
- role: "languageModel",
143
- icon: chatModel.presentation?.icon,
144
- parentNodeId: node.id,
145
- };
183
+ private edgeKey(fromNodeId: string, toNodeId: string, toInput: string): string {
184
+ return `${fromNodeId}\0${toNodeId}\0${toInput}`;
146
185
  }
147
186
 
148
- private createToolNode(node: NodeDefinition, toolConfig: ToolConfig): WorkflowNodeDto {
187
+ private createConnectionNode(connectionNode: AgentConnectionNodeDescriptor): WorkflowNodeDto {
149
188
  return {
150
- id: ConnectionNodeIdFactory.toolConnectionNodeId(node.id, toolConfig.name),
189
+ id: connectionNode.nodeId,
151
190
  kind: "node",
152
- name: toolConfig.presentation?.label ?? toolConfig.name,
153
- type: toolConfig.name,
154
- role: "tool",
155
- icon: toolConfig.presentation?.icon,
156
- parentNodeId: node.id,
191
+ name: connectionNode.name,
192
+ type: connectionNode.typeName,
193
+ role: connectionNode.role,
194
+ icon: connectionNode.icon,
195
+ parentNodeId: connectionNode.parentNodeId,
157
196
  };
158
197
  }
159
198
 
@@ -1,4 +1,4 @@
1
- import type { NodeDefinition, RetryPolicySpec, WorkflowDefinition } from "@codemation/core";
1
+ import type { NodeDefinition, RetryPolicySpec, WorkflowDefinition } from "@codemation/core/browser";
2
2
 
3
3
  /** UI-facing policy labels derived from workflow/node definitions (live or hydrated snapshot). */
4
4
  export class WorkflowPolicyUiPresentationFactory {
@@ -0,0 +1,8 @@
1
+ import type { WorkflowRunDetailDto } from "@codemation/core";
2
+ import { Query } from "../bus/Query";
3
+
4
+ export class GetWorkflowRunDetailQuery extends Query<WorkflowRunDetailDto | undefined> {
5
+ constructor(public readonly runId: string) {
6
+ super();
7
+ }
8
+ }
@@ -0,0 +1,24 @@
1
+ import type { WorkflowRunDetailDto } from "@codemation/core";
2
+ import { inject } from "@codemation/core";
3
+ import { ApplicationTokens } from "../../applicationTokens";
4
+ import type { WorkflowRunRepository } from "../../domain/runs/WorkflowRunRepository";
5
+ import { HandlesQuery } from "../../infrastructure/di/HandlesQueryRegistry";
6
+ import { QueryHandler } from "../bus/QueryHandler";
7
+ import { GetWorkflowRunDetailQuery } from "./GetWorkflowRunDetailQuery";
8
+
9
+ @HandlesQuery.for(GetWorkflowRunDetailQuery)
10
+ export class GetWorkflowRunDetailQueryHandler extends QueryHandler<
11
+ GetWorkflowRunDetailQuery,
12
+ WorkflowRunDetailDto | undefined
13
+ > {
14
+ constructor(
15
+ @inject(ApplicationTokens.WorkflowRunRepository)
16
+ private readonly workflowRunRepository: WorkflowRunRepository,
17
+ ) {
18
+ super();
19
+ }
20
+
21
+ async execute(query: GetWorkflowRunDetailQuery): Promise<WorkflowRunDetailDto | undefined> {
22
+ return await this.workflowRunRepository.loadRunDetail?.(query.runId);
23
+ }
24
+ }
@@ -1,5 +1,6 @@
1
1
  export { GetRunBinaryAttachmentQueryHandler } from "./GetRunBinaryAttachmentQueryHandler";
2
2
  export { GetRunStateQueryHandler } from "./GetRunStateQueryHandler";
3
+ export { GetWorkflowRunDetailQueryHandler } from "./GetWorkflowRunDetailQueryHandler";
3
4
  export { GetWorkflowDebuggerOverlayQueryHandler } from "./GetWorkflowDebuggerOverlayQueryHandler";
4
5
  export { GetWorkflowDetailQueryHandler } from "./GetWorkflowDetailQueryHandler";
5
6
  export { GetWorkflowOverlayBinaryAttachmentQueryHandler } from "./GetWorkflowOverlayBinaryAttachmentQueryHandler";
@@ -1,8 +1,7 @@
1
1
  import type { BinaryStorage, Clock, RunId, WorkflowId } from "@codemation/core";
2
- import { CoreTokens, RunFinishedAtFactory } from "@codemation/core";
2
+ import { CoreTokens } from "@codemation/core";
3
3
  import { inject, injectable } from "@codemation/core";
4
4
  import type { Logger } from "../logging/Logger";
5
- import { RunStateBinaryStorageKeysCollector } from "../binary/RunStateBinaryStorageKeysCollector";
6
5
  import { ApplicationTokens } from "../../applicationTokens";
7
6
  import type { AppConfig } from "../../presentation/config/AppConfig";
8
7
  import type { WorkflowRunRepository } from "../../domain/runs/WorkflowRunRepository";
@@ -17,7 +16,6 @@ import { ServerLoggerFactory } from "../../infrastructure/logging/ServerLoggerFa
17
16
  export class WorkflowRunRetentionPruneScheduler {
18
17
  private timer: ReturnType<typeof setInterval> | undefined;
19
18
  private readonly logger: Logger;
20
- private readonly binaryKeysCollector = new RunStateBinaryStorageKeysCollector();
21
19
 
22
20
  constructor(
23
21
  @inject(ApplicationTokens.Clock) private readonly clock: Clock,
@@ -54,36 +52,24 @@ export class WorkflowRunRetentionPruneScheduler {
54
52
  /** Exposed for tests; production path is the interval started by {@link start}. */
55
53
  async runOnce(): Promise<void> {
56
54
  this.logger.debug("Run retention prune: starting check");
57
-
58
55
  const defaultRetentionSec = Number(this.appConfig.env.CODEMATION_RUN_RETENTION_DEFAULT_SECONDS ?? 86_400);
59
- const summaries = await this.runs.listRuns({ limit: 500 });
60
- const nowMs = this.clock.now().getTime();
56
+ const beforeIso = new Date(this.clock.now().getTime() - defaultRetentionSec * 1000).toISOString();
57
+ const summaries = await this.runs.listRunsOlderThan?.({ beforeIso, limit: 500 });
58
+ const candidates =
59
+ summaries ??
60
+ (await this.runs.listRuns({ limit: 500 })).filter(
61
+ (summary) => summary.status === "completed" || summary.status === "failed",
62
+ );
61
63
 
62
64
  let foundCount = 0;
63
65
  let prunedCount = 0;
64
- for (const s of summaries) {
65
- if (s.status !== "completed" && s.status !== "failed") {
66
- continue;
67
- }
68
- const state = await this.runs.load(s.runId);
69
- if (!state) {
70
- continue;
71
- }
72
- const retentionSec = state.policySnapshot?.retentionSeconds ?? defaultRetentionSec;
73
- const finishedAt = RunFinishedAtFactory.resolveIso(state) ?? s.finishedAt;
74
- if (!finishedAt) {
75
- continue;
76
- }
77
- const ageMs = nowMs - Date.parse(finishedAt);
78
- if (ageMs <= retentionSec * 1000) {
79
- continue;
80
- }
81
-
82
- const runId = s.runId as RunId;
83
- const workflowId = s.workflowId as WorkflowId;
66
+ for (const candidate of candidates) {
67
+ const runId = candidate.runId as RunId;
68
+ const workflowId = candidate.workflowId as WorkflowId;
84
69
  foundCount += 1;
85
70
 
86
- const storageKeys = this.binaryKeysCollector.collectFromRunState(state);
71
+ const storageKeys =
72
+ (await this.runs.listBinaryStorageKeys?.(runId)) ?? (await this.loadStorageKeysFromRunStateFallback(runId));
87
73
  for (const key of storageKeys) {
88
74
  await this.binaryStorage.delete(key);
89
75
  }
@@ -95,4 +81,43 @@ export class WorkflowRunRetentionPruneScheduler {
95
81
  this.logger.info(`Run retention prune: found ${foundCount} run(s) to prune`);
96
82
  this.logger.info(`Run retention prune: pruned ${prunedCount} run(s)`);
97
83
  }
84
+
85
+ private async loadStorageKeysFromRunStateFallback(runId: RunId): Promise<ReadonlyArray<string>> {
86
+ const state = await this.runs.load(runId);
87
+ if (!state) {
88
+ return [];
89
+ }
90
+ const keys = new Set<string>();
91
+ this.collectStorageKeysFromValue(state.outputsByNode, keys);
92
+ this.collectStorageKeysFromValue(state.nodeSnapshotsByNodeId, keys);
93
+ this.collectStorageKeysFromValue(state.mutableState, keys);
94
+ return [...keys];
95
+ }
96
+
97
+ private collectStorageKeysFromValue(value: unknown, keys: Set<string>): void {
98
+ if (Array.isArray(value)) {
99
+ for (const entry of value) {
100
+ this.collectStorageKeysFromValue(entry, keys);
101
+ }
102
+ return;
103
+ }
104
+ if (!value || typeof value !== "object") {
105
+ return;
106
+ }
107
+ const record = value as Record<string, unknown>;
108
+ if (
109
+ typeof record.id === "string" &&
110
+ typeof record.storageKey === "string" &&
111
+ typeof record.mimeType === "string" &&
112
+ typeof record.size === "number"
113
+ ) {
114
+ if (record.storageKey.length > 0) {
115
+ keys.add(record.storageKey);
116
+ }
117
+ return;
118
+ }
119
+ for (const child of Object.values(record)) {
120
+ this.collectStorageKeysFromValue(child, keys);
121
+ }
122
+ }
98
123
  }