@stigmer/server 3.25.0 → 3.26.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 (142) hide show
  1. package/dist/boot/compose.d.ts.map +1 -1
  2. package/dist/boot/compose.js +11 -8
  3. package/dist/boot/compose.js.map +1 -1
  4. package/dist/boot/list-indexes.d.ts +3 -0
  5. package/dist/boot/list-indexes.d.ts.map +1 -0
  6. package/dist/boot/list-indexes.js +24 -0
  7. package/dist/boot/list-indexes.js.map +1 -0
  8. package/dist/domain/agentexecution/controller.d.ts.map +1 -1
  9. package/dist/domain/agentexecution/controller.js +6 -8
  10. package/dist/domain/agentexecution/controller.js.map +1 -1
  11. package/dist/domain/agentexecution/list-index.d.ts +2 -0
  12. package/dist/domain/agentexecution/list-index.d.ts.map +1 -0
  13. package/dist/domain/agentexecution/list-index.js +23 -0
  14. package/dist/domain/agentexecution/list-index.js.map +1 -0
  15. package/dist/domain/agentexecution/steps.d.ts +17 -33
  16. package/dist/domain/agentexecution/steps.d.ts.map +1 -1
  17. package/dist/domain/agentexecution/steps.js +94 -95
  18. package/dist/domain/agentexecution/steps.js.map +1 -1
  19. package/dist/domain/agentexecution/usage.d.ts.map +1 -1
  20. package/dist/domain/agentexecution/usage.js +37 -14
  21. package/dist/domain/agentexecution/usage.js.map +1 -1
  22. package/dist/domain/artifact/controller.d.ts.map +1 -1
  23. package/dist/domain/artifact/controller.js +27 -15
  24. package/dist/domain/artifact/controller.js.map +1 -1
  25. package/dist/domain/artifact/list-index.d.ts +2 -0
  26. package/dist/domain/artifact/list-index.d.ts.map +1 -0
  27. package/dist/domain/artifact/list-index.js +21 -0
  28. package/dist/domain/artifact/list-index.js.map +1 -0
  29. package/dist/domain/session/list-index.d.ts +4 -0
  30. package/dist/domain/session/list-index.d.ts.map +1 -0
  31. package/dist/domain/session/list-index.js +26 -0
  32. package/dist/domain/session/list-index.js.map +1 -0
  33. package/dist/domain/session/steps.d.ts +7 -8
  34. package/dist/domain/session/steps.d.ts.map +1 -1
  35. package/dist/domain/session/steps.js +76 -78
  36. package/dist/domain/session/steps.js.map +1 -1
  37. package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
  38. package/dist/domain/workflowexecution/controller.js +53 -55
  39. package/dist/domain/workflowexecution/controller.js.map +1 -1
  40. package/dist/domain/workflowexecution/execution-filter.d.ts +4 -0
  41. package/dist/domain/workflowexecution/execution-filter.d.ts.map +1 -1
  42. package/dist/domain/workflowexecution/execution-filter.js +11 -5
  43. package/dist/domain/workflowexecution/execution-filter.js.map +1 -1
  44. package/dist/domain/workflowexecution/get-execution-summary.d.ts.map +1 -1
  45. package/dist/domain/workflowexecution/get-execution-summary.js +23 -17
  46. package/dist/domain/workflowexecution/get-execution-summary.js.map +1 -1
  47. package/dist/domain/workflowexecution/list-index.d.ts +2 -0
  48. package/dist/domain/workflowexecution/list-index.d.ts.map +1 -0
  49. package/dist/domain/workflowexecution/list-index.js +21 -0
  50. package/dist/domain/workflowexecution/list-index.js.map +1 -0
  51. package/dist/domain/workflowexecution/list-pending-approvals.d.ts.map +1 -1
  52. package/dist/domain/workflowexecution/list-pending-approvals.js +69 -32
  53. package/dist/domain/workflowexecution/list-pending-approvals.js.map +1 -1
  54. package/dist/domain/workflowexecution/queries.d.ts +30 -4
  55. package/dist/domain/workflowexecution/queries.d.ts.map +1 -1
  56. package/dist/domain/workflowexecution/queries.js +76 -22
  57. package/dist/domain/workflowexecution/queries.js.map +1 -1
  58. package/dist/extensions/list-read-scope.d.ts +9 -12
  59. package/dist/extensions/list-read-scope.d.ts.map +1 -1
  60. package/dist/extensions/list-read-scope.js.map +1 -1
  61. package/dist/index.d.ts +2 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js.map +1 -1
  64. package/dist/pipeline/steps/list-page.d.ts +63 -0
  65. package/dist/pipeline/steps/list-page.d.ts.map +1 -0
  66. package/dist/pipeline/steps/list-page.js +187 -0
  67. package/dist/pipeline/steps/list-page.js.map +1 -0
  68. package/dist/query/activity/handler.d.ts +6 -7
  69. package/dist/query/activity/handler.d.ts.map +1 -1
  70. package/dist/query/activity/handler.js +42 -59
  71. package/dist/query/activity/handler.js.map +1 -1
  72. package/dist/store/interface.d.ts +29 -0
  73. package/dist/store/interface.d.ts.map +1 -1
  74. package/dist/store/interface.js.map +1 -1
  75. package/dist/store/list-index.d.ts +123 -0
  76. package/dist/store/list-index.d.ts.map +1 -0
  77. package/dist/store/list-index.js +277 -0
  78. package/dist/store/list-index.js.map +1 -0
  79. package/dist/store/logger.d.ts +3 -0
  80. package/dist/store/logger.d.ts.map +1 -1
  81. package/dist/store/logger.js +5 -1
  82. package/dist/store/logger.js.map +1 -1
  83. package/dist/store/postgres/migrations.d.ts +10 -6
  84. package/dist/store/postgres/migrations.d.ts.map +1 -1
  85. package/dist/store/postgres/migrations.js +64 -6
  86. package/dist/store/postgres/migrations.js.map +1 -1
  87. package/dist/store/postgres/store.d.ts +26 -5
  88. package/dist/store/postgres/store.d.ts.map +1 -1
  89. package/dist/store/postgres/store.js +377 -22
  90. package/dist/store/postgres/store.js.map +1 -1
  91. package/dist/store/sqlite/migrations.d.ts +11 -5
  92. package/dist/store/sqlite/migrations.d.ts.map +1 -1
  93. package/dist/store/sqlite/migrations.js +50 -4
  94. package/dist/store/sqlite/migrations.js.map +1 -1
  95. package/dist/store/sqlite/store.d.ts +18 -3
  96. package/dist/store/sqlite/store.d.ts.map +1 -1
  97. package/dist/store/sqlite/store.js +321 -15
  98. package/dist/store/sqlite/store.js.map +1 -1
  99. package/package.json +6 -6
  100. package/src/authorization/__tests__/drivers.ts +6 -1
  101. package/src/boot/__tests__/list-indexes.test.ts +65 -0
  102. package/src/boot/compose.ts +23 -13
  103. package/src/boot/list-indexes.ts +25 -0
  104. package/src/domain/agentexecution/__tests__/agentexecution.test.ts +85 -3
  105. package/src/domain/agentexecution/controller.ts +8 -10
  106. package/src/domain/agentexecution/list-index.ts +24 -0
  107. package/src/domain/agentexecution/steps.ts +118 -115
  108. package/src/domain/agentexecution/usage.ts +38 -17
  109. package/src/domain/artifact/controller.ts +28 -18
  110. package/src/domain/artifact/list-index.ts +22 -0
  111. package/src/domain/session/list-index.ts +28 -0
  112. package/src/domain/session/steps.ts +108 -123
  113. package/src/domain/workflowexecution/__tests__/list-read-scope-summaries.test.ts +155 -99
  114. package/src/domain/workflowexecution/__tests__/workflowexecution.test.ts +193 -31
  115. package/src/domain/workflowexecution/controller.ts +54 -82
  116. package/src/domain/workflowexecution/execution-filter.ts +20 -7
  117. package/src/domain/workflowexecution/get-execution-summary.ts +31 -24
  118. package/src/domain/workflowexecution/list-index.ts +22 -0
  119. package/src/domain/workflowexecution/list-pending-approvals.ts +82 -25
  120. package/src/domain/workflowexecution/queries.ts +134 -23
  121. package/src/extensions/list-read-scope.ts +9 -12
  122. package/src/index.ts +10 -1
  123. package/src/pipeline/steps/__tests__/list-page.test.ts +186 -0
  124. package/src/pipeline/steps/list-page.ts +284 -0
  125. package/src/query/activity/handler.ts +57 -78
  126. package/src/store/README.md +51 -32
  127. package/src/store/__tests__/list-index.measure.test.ts +293 -0
  128. package/src/store/__tests__/list-index.test.ts +328 -0
  129. package/src/store/__tests__/store-contract.ts +346 -3
  130. package/src/store/interface.ts +39 -0
  131. package/src/store/list-index.ts +465 -0
  132. package/src/store/logger.ts +8 -1
  133. package/src/store/postgres/__tests__/list-index-repair.test.ts +133 -0
  134. package/src/store/postgres/__tests__/migrations.test.ts +27 -19
  135. package/src/store/postgres/__tests__/store-contract.test.ts +50 -3
  136. package/src/store/postgres/migrations.ts +69 -7
  137. package/src/store/postgres/store.ts +485 -29
  138. package/src/store/sqlite/__tests__/migrations.test.ts +51 -7
  139. package/src/store/sqlite/__tests__/store-contract.test.ts +70 -16
  140. package/src/store/sqlite/__tests__/support.ts +12 -3
  141. package/src/store/sqlite/migrations.ts +57 -6
  142. package/src/store/sqlite/store.ts +462 -21
@@ -3,10 +3,14 @@
3
3
  * list_by_session.go, plus the full-scan load helper the usage reports
4
4
  * share.
5
5
  *
6
- * List semantics ported verbatim: no sorting (unlike the session domain's
7
- * newest-first list), no pagination (total_pages pinned to 1 — Go's
8
- * placeholder), the request `org` field a deliberate no-op (single-tenant
9
- * edition), and phase filtering only when a phase is specified.
6
+ * Both lists read through the list index (list-index.ts beside this file;
7
+ * the store's contract in store/interface.ts), newest first by spec-audit
8
+ * created_at. `list` pages (pipeline/steps/list-page.ts): the store
9
+ * narrows by the request's org, the phase filter and then the read scope
10
+ * run on each batch — the scope last, so a composed driver is asked about
11
+ * the rows the request keeps. `listBySession` returns the session's
12
+ * executions whole: a conversation is read as one, and its consumers (the
13
+ * transcript, the channel and guest turn limits) need all of it.
10
14
  */
11
15
  import { create, fromBinary } from "@bufbuild/protobuf";
12
16
 
@@ -22,12 +26,19 @@ import type { ListReadScope } from "../../extensions/list-read-scope.js";
22
26
  import { restrictListByReadScope } from "../../extensions/list-read-scope.js";
23
27
  import { internalError, invalidArgumentError } from "../../pipeline/errors.js";
24
28
  import type { PipelineStep } from "../../pipeline/pipeline.js";
29
+ import {
30
+ listPageFingerprint,
31
+ readListPage,
32
+ } from "../../pipeline/steps/list-page.js";
33
+ import type { ListPage } from "../../pipeline/steps/list-page.js";
25
34
  import type { Store } from "../../store/interface.js";
35
+ import type { ListIndexRow } from "../../store/list-index.js";
36
+ import { agentExecutionListIndex } from "./list-index.js";
26
37
 
27
38
  /**
28
- * Inter-step key for the working execution list AND the final
29
- * AgentExecutionList — Go reuses one key ("execution_list") for both, and
30
- * the controller reads the final response from it.
39
+ * Inter-step key for the working page AND the final AgentExecutionList —
40
+ * Go reuses one key ("execution_list") for both, and the controller reads
41
+ * the final response from it.
31
42
  */
32
43
  export const EXECUTION_LIST_KEY = "execution_list";
33
44
 
@@ -36,8 +47,9 @@ type ListBySessionDesc =
36
47
  typeof AgentExecutionQueryController.method.listBySession.input;
37
48
 
38
49
  /**
39
- * Full-scan agent-execution load shared by the list steps and the usage
40
- * reports; malformed rows warn + skip (Go's proto.Unmarshal-continue).
50
+ * Full-scan agent-execution load for the usage reports, whose org match
51
+ * is case-insensitive (Go strings.EqualFold) where the list index's is
52
+ * exact; malformed rows warn + skip (Go's proto.Unmarshal-continue).
41
53
  */
42
54
  export async function loadAllAgentExecutions(
43
55
  store: Store,
@@ -49,21 +61,7 @@ export async function loadAllAgentExecutions(
49
61
  } catch (error) {
50
62
  throw internalError(error, "failed to list agent executions");
51
63
  }
52
-
53
- const executions: AgentExecution[] = [];
54
- for (const data of rows) {
55
- let execution: AgentExecution;
56
- try {
57
- execution = fromBinary(AgentExecutionSchema, data);
58
- } catch (error) {
59
- logger.warn("Failed to unmarshal execution, skipping", {
60
- error: error instanceof Error ? error.message : String(error),
61
- });
62
- continue;
63
- }
64
- executions.push(execution);
65
- }
66
- return executions;
64
+ return decodeExecutions(rows, logger);
67
65
  }
68
66
 
69
67
  /** ValidateListRequest — list.go: no required fields today (a no-op). */
@@ -71,92 +69,54 @@ export function newValidateListRequestStep(): PipelineStep<ListDesc> {
71
69
  return {
72
70
  name: "ValidateListRequest",
73
71
  execute() {
74
- // Go validates nothing here yet; pagination validation would land
75
- // in both editions together.
72
+ // Go validates nothing here yet; page_size is protovalidate's and
73
+ // page_token the page reader's.
76
74
  },
77
75
  };
78
76
  }
79
77
 
80
78
  /**
81
- * QueryAllExecutions — list.go: full scan into the context. The read
82
- * scope no longer runs here: since stigmer-cloud 20260913.04 T02 the scope
83
- * is the LAST per-row predicate of the chain (RestrictByReadScope below,
84
- * after ApplyPhaseFilter), so a composed driver is offered the request's
85
- * org and phase, not the kind (extensions/list-read-scope.ts, the
86
- * header's last contract line).
79
+ * QueryExecutionPage — one page of the request's executions: its org
80
+ * through the index when it names one, its phase when it names one, the
81
+ * read scope last (census lane 4; with no scope composed, every matching
82
+ * execution — the OSS single-user posture).
87
83
  */
88
- export function newQueryAllExecutionsStep(
84
+ export function newQueryExecutionPageStep(
89
85
  store: Store,
90
86
  logger: Logger,
91
- ): PipelineStep<ListDesc> {
92
- return {
93
- name: "QueryAllExecutions",
94
- async execute(ctx) {
95
- const executions = await loadAllAgentExecutions(store, logger);
96
- logger.debug("Successfully queried executions", {
97
- count: executions.length,
98
- });
99
- ctx.set(EXECUTION_LIST_KEY, executions);
100
- },
101
- };
102
- }
103
-
104
- /**
105
- * RestrictByReadScope — the list chain's last per-row predicate (census
106
- * lane 4). With a composed ListReadScope (20260830.01) the working list
107
- * narrows to the caller's authorized executions ∩ the request's org (the
108
- * Java AgentExecutionListHandler baseline: org set = one-org view, blank
109
- * = permission-bounded across orgs; the guest cookie rule rides the
110
- * driver); no scope = the working list unchanged, org a no-op —
111
- * byte-identical. Placed after ApplyPhaseFilter so the driver is asked
112
- * about the rows the request keeps, never about the whole kind.
113
- */
114
- export function newRestrictByReadScopeStep(
115
87
  listReadScope: ListReadScope | undefined,
116
88
  ): PipelineStep<ListDesc> {
117
89
  return {
118
- name: "RestrictByReadScope",
90
+ name: "QueryExecutionPage",
119
91
  async execute(ctx) {
120
- const executions = requireExecutions(ctx.get(EXECUTION_LIST_KEY));
121
- ctx.set(
122
- EXECUTION_LIST_KEY,
123
- await restrictListByReadScope(
124
- listReadScope,
125
- ctx.callerIdentity,
126
- ApiResourceKind.agent_execution,
127
- executions,
128
- ctx.input.org,
129
- ),
130
- );
131
- },
132
- };
133
- }
134
-
135
- /**
136
- * ApplyPhaseFilter — list.go: status.phase equality filter, skipped when
137
- * the request carries no phase.
138
- */
139
- export function newApplyPhaseFilterStep(
140
- logger: Logger,
141
- ): PipelineStep<ListDesc> {
142
- return {
143
- name: "ApplyPhaseFilter",
144
- execute(ctx) {
145
- const executions = requireExecutions(ctx.get(EXECUTION_LIST_KEY));
146
- if (ctx.input.phase === ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED) {
147
- logger.debug("No phase filter specified, skipping");
148
- return;
149
- }
150
- const filtered = executions.filter(
151
- (execution) =>
92
+ const input = ctx.input;
93
+ const page = await readListPage({
94
+ store,
95
+ declaration: agentExecutionListIndex,
96
+ query: { org: input.org },
97
+ request: input,
98
+ fingerprint: listPageFingerprint({
99
+ lane: "agentExecution.list",
100
+ org: input.org,
101
+ phase: input.phase,
102
+ tags: input.tags,
103
+ }),
104
+ decode: (data) => decodeExecution(data, logger),
105
+ keep: (execution) =>
106
+ input.phase === ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED ||
152
107
  (execution.status?.phase ??
153
- ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED) === ctx.input.phase,
154
- );
155
- logger.debug("Phase filter applied", {
156
- originalCount: executions.length,
157
- filteredCount: filtered.length,
108
+ ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED) === input.phase,
109
+ scope: (executions) =>
110
+ restrictListByReadScope(
111
+ listReadScope,
112
+ ctx.callerIdentity,
113
+ ApiResourceKind.agent_execution,
114
+ executions,
115
+ "",
116
+ ),
117
+ failure: "failed to list agent executions",
158
118
  });
159
- ctx.set(EXECUTION_LIST_KEY, filtered);
119
+ ctx.set(EXECUTION_LIST_KEY, page);
160
120
  },
161
121
  };
162
122
  }
@@ -176,9 +136,9 @@ export function newValidateListBySessionRequestStep(): PipelineStep<ListBySessio
176
136
  }
177
137
 
178
138
  /**
179
- * QueryExecutionsBySession — list_by_session.go: full scan +
180
- * spec.session_id equality filter, then the read scope over the session's
181
- * executions only (the scope is the last per-row predicate).
139
+ * QueryExecutionsBySession — list_by_session.go: the session's executions
140
+ * through the index's session key, whole and newest first, then the read
141
+ * scope (census lane 5; bounded by the session id, org never consulted).
182
142
  */
183
143
  export function newQueryExecutionsBySessionStep(
184
144
  store: Store,
@@ -188,31 +148,41 @@ export function newQueryExecutionsBySessionStep(
188
148
  return {
189
149
  name: "QueryExecutionsBySession",
190
150
  async execute(ctx) {
191
- // Census lane 5: session filter ∩ authorized ids; the Java handler
192
- // never consults org on this lane (bounded by the session id).
193
- const ofSession = (await loadAllAgentExecutions(store, logger)).filter(
194
- (execution) =>
195
- (execution.spec?.sessionId ?? "") === ctx.input.sessionId,
196
- );
151
+ let rows: ListIndexRow[];
152
+ try {
153
+ rows = await store.queryResources(agentExecutionListIndex, {
154
+ anyKey: [{ name: "session", value: ctx.input.sessionId }],
155
+ });
156
+ } catch (error) {
157
+ throw internalError(error, "failed to list agent executions");
158
+ }
197
159
  const executions = await restrictListByReadScope(
198
160
  listReadScope,
199
161
  ctx.callerIdentity,
200
162
  ApiResourceKind.agent_execution,
201
- ofSession,
163
+ decodeExecutions(
164
+ rows.map((row) => row.data),
165
+ logger,
166
+ ),
202
167
  "",
203
168
  );
204
169
  logger.debug("Successfully queried executions by session", {
205
170
  sessionId: ctx.input.sessionId,
206
171
  count: executions.length,
207
172
  });
208
- ctx.set(EXECUTION_LIST_KEY, executions);
173
+ const page: ListPage<AgentExecution> = {
174
+ entries: executions,
175
+ nextPageToken: "",
176
+ };
177
+ ctx.set(EXECUTION_LIST_KEY, page);
209
178
  },
210
179
  };
211
180
  }
212
181
 
213
182
  /**
214
- * BuildExecutionListResponse — both list files: wraps the working list
215
- * into AgentExecutionList with the total_pages placeholder pinned to 1.
183
+ * BuildExecutionListResponse — both list files: wraps the page into
184
+ * AgentExecutionList; total_pages is 1 when the response is whole and 0
185
+ * when a token follows (the contract's "not computed").
216
186
  */
217
187
  export function newBuildExecutionListResponseStep<
218
188
  Desc extends ListDesc | ListBySessionDesc,
@@ -220,24 +190,57 @@ export function newBuildExecutionListResponseStep<
220
190
  return {
221
191
  name: "BuildExecutionListResponse",
222
192
  execute(ctx) {
223
- const executions = requireExecutions(ctx.get(EXECUTION_LIST_KEY));
193
+ const page = requirePage(ctx.get(EXECUTION_LIST_KEY));
224
194
  ctx.set(
225
195
  EXECUTION_LIST_KEY,
226
196
  create(AgentExecutionListSchema, {
227
- totalPages: 1,
228
- entries: executions,
197
+ totalPages: page.nextPageToken === "" ? 1 : 0,
198
+ entries: page.entries,
199
+ nextPageToken: page.nextPageToken,
229
200
  }),
230
201
  );
231
202
  },
232
203
  };
233
204
  }
234
205
 
235
- function requireExecutions(value: unknown): AgentExecution[] {
236
- if (!Array.isArray(value)) {
206
+ function decodeExecution(
207
+ data: Uint8Array,
208
+ logger: Logger,
209
+ ): AgentExecution | undefined {
210
+ try {
211
+ return fromBinary(AgentExecutionSchema, data);
212
+ } catch (error) {
213
+ logger.warn("Failed to unmarshal execution, skipping", {
214
+ error: error instanceof Error ? error.message : String(error),
215
+ });
216
+ return undefined;
217
+ }
218
+ }
219
+
220
+ function decodeExecutions(
221
+ rows: ReadonlyArray<Uint8Array>,
222
+ logger: Logger,
223
+ ): AgentExecution[] {
224
+ const executions: AgentExecution[] = [];
225
+ for (const data of rows) {
226
+ const execution = decodeExecution(data, logger);
227
+ if (execution !== undefined) {
228
+ executions.push(execution);
229
+ }
230
+ }
231
+ return executions;
232
+ }
233
+
234
+ function requirePage(value: unknown): ListPage<AgentExecution> {
235
+ if (
236
+ typeof value !== "object" ||
237
+ value === null ||
238
+ !Array.isArray((value as { entries?: unknown }).entries)
239
+ ) {
237
240
  throw internalError(
238
241
  new Error("execution list not found in context"),
239
242
  "execution list not found in context",
240
243
  );
241
244
  }
242
- return value as AgentExecution[];
245
+ return value as ListPage<AgentExecution>;
243
246
  }
@@ -67,6 +67,7 @@ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/
67
67
  import type { Logger } from "../../boot/logger.js";
68
68
  import type { Authorizer } from "../../extensions/authorizer.js";
69
69
  import type { ListReadScope } from "../../extensions/list-read-scope.js";
70
+ import { restrictListByReadScope } from "../../extensions/list-read-scope.js";
70
71
  import { internalError, invalidArgumentError } from "../../pipeline/errors.js";
71
72
  import type { PipelineStep } from "../../pipeline/pipeline.js";
72
73
  import { newPipeline } from "../../pipeline/pipeline.js";
@@ -74,8 +75,11 @@ import type { CallerIdentity } from "../../extensions/identity.js";
74
75
  import { RequestContext } from "../../pipeline/request-context.js";
75
76
  import { newAuthorizeStep } from "../../pipeline/steps/authorize.js";
76
77
  import type { Store } from "../../store/interface.js";
78
+ import { listIndexInstantOfMillis } from "../../store/list-index.js";
79
+ import type { ListIndexRow } from "../../store/list-index.js";
77
80
 
78
81
  import { executionUsageReportNotFoundMessage } from "./constants.js";
82
+ import { agentExecutionListIndex } from "./list-index.js";
79
83
  import { EXECUTION_LIST_KEY, loadAllAgentExecutions } from "./steps.js";
80
84
 
81
85
  export interface UsageReportDeps {
@@ -781,13 +785,11 @@ function requireReport<T>(value: unknown, message: string): T {
781
785
  // absent from this shape (AD-DASH-005: the dashboard sources cost from
782
786
  // getOrgUsageReport to prevent double-counting).
783
787
  //
784
- // With a composed ListReadScope (C2 Stage 4's ExecutionReadScope,
785
- // absorbed by 20260830.01), the scan narrows to
786
- // the caller's authorized ids ∩ the requested org and an empty set
787
- // answers the default instance — the Java
788
- // AgentExecutionGetExecutionSummaryHandler baseline; see the twin's
789
- // header (workflowexecution/get-execution-summary.ts) for the full
790
- // rationale. No scope composed = this full scan, byte-identical.
788
+ // The rows are the requested org's within the time window, read through
789
+ // the list index; with a composed ListReadScope they are offered to its
790
+ // restrict verb, and none visible answers the default instance — the Java
791
+ // AgentExecutionGetExecutionSummaryHandler baseline; see the twin's header
792
+ // (workflowexecution/get-execution-summary.ts) for the full rationale.
791
793
  // ---------------------------------------------------------------------------
792
794
 
793
795
  export async function getExecutionSummary(
@@ -795,25 +797,44 @@ export async function getExecutionSummary(
795
797
  req: GetAgentExecutionSummaryRequest,
796
798
  identity: CallerIdentity,
797
799
  ): Promise<AgentExecutionSummary> {
798
- let executions = await loadAllAgentExecutions(deps.store, deps.logger);
800
+ // The store's window is one millisecond wider than the loop's check
801
+ // below, which stays the authority: it only ever narrows the read.
802
+ const cutoffMs = resolveAgentTimeCutoffMs(req.timeWindow);
803
+ let rows: ListIndexRow[];
804
+ try {
805
+ rows = await deps.store.queryResources(agentExecutionListIndex, {
806
+ org: req.org,
807
+ ...(cutoffMs === undefined
808
+ ? {}
809
+ : { createdAtOrAfter: listIndexInstantOfMillis(cutoffMs - 1) }),
810
+ });
811
+ } catch (error) {
812
+ throw internalError(error, "failed to list agent executions");
813
+ }
814
+ let executions: AgentExecution[] = [];
815
+ for (const row of rows) {
816
+ try {
817
+ executions.push(fromBinary(AgentExecutionSchema, row.data));
818
+ } catch (error) {
819
+ deps.logger.warn("Failed to unmarshal execution, skipping", {
820
+ error: error instanceof Error ? error.message : String(error),
821
+ });
822
+ }
823
+ }
799
824
 
800
825
  if (deps.listReadScope !== undefined) {
801
- const authorizedIds = await deps.listReadScope.authorizedResourceIds(
826
+ executions = await restrictListByReadScope(
827
+ deps.listReadScope,
802
828
  identity,
803
829
  ApiResourceKind.agent_execution,
830
+ executions,
831
+ "",
804
832
  );
805
- if (authorizedIds.size === 0) {
833
+ if (executions.length === 0) {
806
834
  return create(AgentExecutionSummarySchema);
807
835
  }
808
- executions = executions.filter(
809
- (execution) =>
810
- authorizedIds.has(execution.metadata?.id ?? "") &&
811
- execution.metadata?.org === req.org,
812
- );
813
836
  }
814
837
 
815
- const cutoffMs = resolveAgentTimeCutoffMs(req.timeWindow);
816
-
817
838
  const phaseCounts: { [key: number]: number } = {};
818
839
  let activeCount = 0;
819
840
  const completedDurationsMs: number[] = [];
@@ -80,6 +80,8 @@ import {
80
80
  } from "../../pipeline/steps/load-target.js";
81
81
  import { newValidateProtoStep } from "../../pipeline/steps/validation.js";
82
82
  import type { Store } from "../../store/interface.js";
83
+ import type { ListIndexRow } from "../../store/list-index.js";
84
+ import { artifactListIndex } from "./list-index.js";
83
85
  import {
84
86
  ARTIFACT_KIND,
85
87
  DEFAULT_MAX_CONTENT_BYTES,
@@ -457,36 +459,44 @@ async function listByExecution(
457
459
  .addStep({
458
460
  name: "QueryArtifactsByExecution",
459
461
  async execute(stepCtx): Promise<void> {
462
+ // The execution's artifacts through the list index's source key,
463
+ // whole and newest first: an execution's artifacts are returned as
464
+ // one (the request's paging fields are not read).
460
465
  const input = stepCtx.input;
461
- let data: Uint8Array[];
466
+ let rows: ListIndexRow[];
462
467
  try {
463
- data = await deps.store.listResources(ApiResourceKind.artifact);
468
+ rows = await deps.store.queryResources(artifactListIndex, {
469
+ anyKey: [
470
+ ...(input.workflowExecutionId === ""
471
+ ? []
472
+ : [
473
+ {
474
+ name: "workflow_execution" as const,
475
+ value: input.workflowExecutionId,
476
+ },
477
+ ]),
478
+ ...(input.agentExecutionId === ""
479
+ ? []
480
+ : [
481
+ {
482
+ name: "agent_execution" as const,
483
+ value: input.agentExecutionId,
484
+ },
485
+ ]),
486
+ ],
487
+ });
464
488
  } catch (error) {
465
489
  throw internalError(error, "failed to list artifacts");
466
490
  }
467
491
 
468
492
  const artifacts: Artifact[] = [];
469
- for (const bytes of data) {
470
- let artifact: Artifact;
493
+ for (const row of rows) {
471
494
  try {
472
- artifact = fromBinary(ArtifactSchema, bytes);
495
+ artifacts.push(fromBinary(ArtifactSchema, row.data));
473
496
  } catch (error) {
474
497
  deps.logger.warn("failed to unmarshal artifact, skipping", {
475
498
  error: error instanceof Error ? error.message : String(error),
476
499
  });
477
- continue;
478
- }
479
- const source = artifact.spec?.source;
480
- if (
481
- input.workflowExecutionId !== "" &&
482
- source?.workflowExecutionId === input.workflowExecutionId
483
- ) {
484
- artifacts.push(artifact);
485
- } else if (
486
- input.agentExecutionId !== "" &&
487
- source?.agentExecutionId === input.agentExecutionId
488
- ) {
489
- artifacts.push(artifact);
490
500
  }
491
501
  }
492
502
 
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The artifact list index (store/list-index.ts): an artifact names the
3
+ * execution that produced it, agent or workflow, and
4
+ * `listByExecution` reads the one the request names.
5
+ *
6
+ * A change to `keys` bumps `revision` (boot/__tests__/list-indexes.test.ts
7
+ * pins the pair).
8
+ */
9
+ import { ArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/artifact/v1/api_pb";
10
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
11
+
12
+ import { declareListIndex, field } from "../../store/list-index.js";
13
+
14
+ export const artifactListIndex = declareListIndex({
15
+ kind: ApiResourceKind.artifact,
16
+ schema: ArtifactSchema,
17
+ revision: 1,
18
+ keys: {
19
+ agent_execution: field("spec.source.agent_execution_id"),
20
+ workflow_execution: field("spec.source.workflow_execution_id"),
21
+ },
22
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The session list index (store/list-index.ts): `agent_instance` is the
3
+ * key `listByAgentInstance` reads, `channel` the one `listByChannel` reads
4
+ * (the channel's id is stamped as a label by the channel lanes, not held
5
+ * in the spec). A session belongs to one organization, which is every
6
+ * declaration's first fact, so `session.list` narrows by it when the
7
+ * request names one.
8
+ *
9
+ * A change to `keys` bumps `revision` (boot/__tests__/list-indexes.test.ts
10
+ * pins the pair).
11
+ */
12
+ import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
13
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
14
+
15
+ import { declareListIndex, field, label } from "../../store/list-index.js";
16
+
17
+ /** The label the channel lanes stamp on every session they open. */
18
+ export const SESSION_CHANNEL_ID_LABEL = "stigmer.ai/channel-id";
19
+
20
+ export const sessionListIndex = declareListIndex({
21
+ kind: ApiResourceKind.session,
22
+ schema: SessionSchema,
23
+ revision: 1,
24
+ keys: {
25
+ agent_instance: field("spec.agent_instance_id"),
26
+ channel: label(SESSION_CHANNEL_ID_LABEL),
27
+ },
28
+ });