@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.
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +11 -8
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/list-indexes.d.ts +3 -0
- package/dist/boot/list-indexes.d.ts.map +1 -0
- package/dist/boot/list-indexes.js +24 -0
- package/dist/boot/list-indexes.js.map +1 -0
- package/dist/domain/agentexecution/controller.d.ts.map +1 -1
- package/dist/domain/agentexecution/controller.js +6 -8
- package/dist/domain/agentexecution/controller.js.map +1 -1
- package/dist/domain/agentexecution/list-index.d.ts +2 -0
- package/dist/domain/agentexecution/list-index.d.ts.map +1 -0
- package/dist/domain/agentexecution/list-index.js +23 -0
- package/dist/domain/agentexecution/list-index.js.map +1 -0
- package/dist/domain/agentexecution/steps.d.ts +17 -33
- package/dist/domain/agentexecution/steps.d.ts.map +1 -1
- package/dist/domain/agentexecution/steps.js +94 -95
- package/dist/domain/agentexecution/steps.js.map +1 -1
- package/dist/domain/agentexecution/usage.d.ts.map +1 -1
- package/dist/domain/agentexecution/usage.js +37 -14
- package/dist/domain/agentexecution/usage.js.map +1 -1
- package/dist/domain/artifact/controller.d.ts.map +1 -1
- package/dist/domain/artifact/controller.js +27 -15
- package/dist/domain/artifact/controller.js.map +1 -1
- package/dist/domain/artifact/list-index.d.ts +2 -0
- package/dist/domain/artifact/list-index.d.ts.map +1 -0
- package/dist/domain/artifact/list-index.js +21 -0
- package/dist/domain/artifact/list-index.js.map +1 -0
- package/dist/domain/session/list-index.d.ts +4 -0
- package/dist/domain/session/list-index.d.ts.map +1 -0
- package/dist/domain/session/list-index.js +26 -0
- package/dist/domain/session/list-index.js.map +1 -0
- package/dist/domain/session/steps.d.ts +7 -8
- package/dist/domain/session/steps.d.ts.map +1 -1
- package/dist/domain/session/steps.js +76 -78
- package/dist/domain/session/steps.js.map +1 -1
- package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
- package/dist/domain/workflowexecution/controller.js +53 -55
- package/dist/domain/workflowexecution/controller.js.map +1 -1
- package/dist/domain/workflowexecution/execution-filter.d.ts +4 -0
- package/dist/domain/workflowexecution/execution-filter.d.ts.map +1 -1
- package/dist/domain/workflowexecution/execution-filter.js +11 -5
- package/dist/domain/workflowexecution/execution-filter.js.map +1 -1
- package/dist/domain/workflowexecution/get-execution-summary.d.ts.map +1 -1
- package/dist/domain/workflowexecution/get-execution-summary.js +23 -17
- package/dist/domain/workflowexecution/get-execution-summary.js.map +1 -1
- package/dist/domain/workflowexecution/list-index.d.ts +2 -0
- package/dist/domain/workflowexecution/list-index.d.ts.map +1 -0
- package/dist/domain/workflowexecution/list-index.js +21 -0
- package/dist/domain/workflowexecution/list-index.js.map +1 -0
- package/dist/domain/workflowexecution/list-pending-approvals.d.ts.map +1 -1
- package/dist/domain/workflowexecution/list-pending-approvals.js +69 -32
- package/dist/domain/workflowexecution/list-pending-approvals.js.map +1 -1
- package/dist/domain/workflowexecution/queries.d.ts +30 -4
- package/dist/domain/workflowexecution/queries.d.ts.map +1 -1
- package/dist/domain/workflowexecution/queries.js +76 -22
- package/dist/domain/workflowexecution/queries.js.map +1 -1
- package/dist/extensions/list-read-scope.d.ts +9 -12
- package/dist/extensions/list-read-scope.d.ts.map +1 -1
- package/dist/extensions/list-read-scope.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline/steps/list-page.d.ts +63 -0
- package/dist/pipeline/steps/list-page.d.ts.map +1 -0
- package/dist/pipeline/steps/list-page.js +187 -0
- package/dist/pipeline/steps/list-page.js.map +1 -0
- package/dist/query/activity/handler.d.ts +6 -7
- package/dist/query/activity/handler.d.ts.map +1 -1
- package/dist/query/activity/handler.js +42 -59
- package/dist/query/activity/handler.js.map +1 -1
- package/dist/store/interface.d.ts +29 -0
- package/dist/store/interface.d.ts.map +1 -1
- package/dist/store/interface.js.map +1 -1
- package/dist/store/list-index.d.ts +123 -0
- package/dist/store/list-index.d.ts.map +1 -0
- package/dist/store/list-index.js +277 -0
- package/dist/store/list-index.js.map +1 -0
- package/dist/store/logger.d.ts +3 -0
- package/dist/store/logger.d.ts.map +1 -1
- package/dist/store/logger.js +5 -1
- package/dist/store/logger.js.map +1 -1
- package/dist/store/postgres/migrations.d.ts +10 -6
- package/dist/store/postgres/migrations.d.ts.map +1 -1
- package/dist/store/postgres/migrations.js +64 -6
- package/dist/store/postgres/migrations.js.map +1 -1
- package/dist/store/postgres/store.d.ts +26 -5
- package/dist/store/postgres/store.d.ts.map +1 -1
- package/dist/store/postgres/store.js +377 -22
- package/dist/store/postgres/store.js.map +1 -1
- package/dist/store/sqlite/migrations.d.ts +11 -5
- package/dist/store/sqlite/migrations.d.ts.map +1 -1
- package/dist/store/sqlite/migrations.js +50 -4
- package/dist/store/sqlite/migrations.js.map +1 -1
- package/dist/store/sqlite/store.d.ts +18 -3
- package/dist/store/sqlite/store.d.ts.map +1 -1
- package/dist/store/sqlite/store.js +321 -15
- package/dist/store/sqlite/store.js.map +1 -1
- package/package.json +6 -6
- package/src/authorization/__tests__/drivers.ts +6 -1
- package/src/boot/__tests__/list-indexes.test.ts +65 -0
- package/src/boot/compose.ts +23 -13
- package/src/boot/list-indexes.ts +25 -0
- package/src/domain/agentexecution/__tests__/agentexecution.test.ts +85 -3
- package/src/domain/agentexecution/controller.ts +8 -10
- package/src/domain/agentexecution/list-index.ts +24 -0
- package/src/domain/agentexecution/steps.ts +118 -115
- package/src/domain/agentexecution/usage.ts +38 -17
- package/src/domain/artifact/controller.ts +28 -18
- package/src/domain/artifact/list-index.ts +22 -0
- package/src/domain/session/list-index.ts +28 -0
- package/src/domain/session/steps.ts +108 -123
- package/src/domain/workflowexecution/__tests__/list-read-scope-summaries.test.ts +155 -99
- package/src/domain/workflowexecution/__tests__/workflowexecution.test.ts +193 -31
- package/src/domain/workflowexecution/controller.ts +54 -82
- package/src/domain/workflowexecution/execution-filter.ts +20 -7
- package/src/domain/workflowexecution/get-execution-summary.ts +31 -24
- package/src/domain/workflowexecution/list-index.ts +22 -0
- package/src/domain/workflowexecution/list-pending-approvals.ts +82 -25
- package/src/domain/workflowexecution/queries.ts +134 -23
- package/src/extensions/list-read-scope.ts +9 -12
- package/src/index.ts +10 -1
- package/src/pipeline/steps/__tests__/list-page.test.ts +186 -0
- package/src/pipeline/steps/list-page.ts +284 -0
- package/src/query/activity/handler.ts +57 -78
- package/src/store/README.md +51 -32
- package/src/store/__tests__/list-index.measure.test.ts +293 -0
- package/src/store/__tests__/list-index.test.ts +328 -0
- package/src/store/__tests__/store-contract.ts +346 -3
- package/src/store/interface.ts +39 -0
- package/src/store/list-index.ts +465 -0
- package/src/store/logger.ts +8 -1
- package/src/store/postgres/__tests__/list-index-repair.test.ts +133 -0
- package/src/store/postgres/__tests__/migrations.test.ts +27 -19
- package/src/store/postgres/__tests__/store-contract.test.ts +50 -3
- package/src/store/postgres/migrations.ts +69 -7
- package/src/store/postgres/store.ts +485 -29
- package/src/store/sqlite/__tests__/migrations.test.ts +51 -7
- package/src/store/sqlite/__tests__/store-contract.test.ts +70 -16
- package/src/store/sqlite/__tests__/support.ts +12 -3
- package/src/store/sqlite/migrations.ts +57 -6
- package/src/store/sqlite/store.ts +462 -21
|
@@ -22,8 +22,6 @@ import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
|
22
22
|
import { WorkflowExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/api_pb";
|
|
23
23
|
import type { WorkflowExecution } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/api_pb";
|
|
24
24
|
import { WorkflowExecutionCommandController } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/command_pb";
|
|
25
|
-
import { ExecutionSortField } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/io_pb";
|
|
26
|
-
import { WorkflowExecutionListSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/io_pb";
|
|
27
25
|
import type {
|
|
28
26
|
ListWorkflowExecutionsByWorkflowRequest,
|
|
29
27
|
ListWorkflowExecutionsRequest,
|
|
@@ -40,7 +38,6 @@ import type { Authorizer } from "../../extensions/authorizer.js";
|
|
|
40
38
|
import type { ResolvedGateSteps } from "../../extensions/gate-slots.js";
|
|
41
39
|
import { stepsForSlot } from "../../extensions/gate-slots.js";
|
|
42
40
|
import type { ListReadScope } from "../../extensions/list-read-scope.js";
|
|
43
|
-
import { restrictListByReadScope } from "../../extensions/list-read-scope.js";
|
|
44
41
|
import type { CallerIdentity } from "../../extensions/identity.js";
|
|
45
42
|
import type { ResourceAuthorizationLifecycle } from "../../extensions/resource-authorization.js";
|
|
46
43
|
import { internalError, invalidArgumentError } from "../../pipeline/errors.js";
|
|
@@ -102,9 +99,8 @@ import { newCreateExecutionContextStep } from "./create-execution-context-step.j
|
|
|
102
99
|
import type { WorkflowExecutionEngineStateProvider } from "./engine.js";
|
|
103
100
|
import { newEnsureEngineAvailableStep } from "./engine.js";
|
|
104
101
|
import {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
applySortField,
|
|
102
|
+
matchesFilterCriteria,
|
|
103
|
+
matchesLegacyPhase,
|
|
108
104
|
} from "./execution-filter.js";
|
|
109
105
|
import {
|
|
110
106
|
cancelExecution,
|
|
@@ -119,7 +115,7 @@ import { sendSignal } from "./send-signal.js";
|
|
|
119
115
|
import { getEventLog } from "./get-event-log.js";
|
|
120
116
|
import { getExecutionSummary } from "./get-execution-summary.js";
|
|
121
117
|
import { listPendingApprovals } from "./list-pending-approvals.js";
|
|
122
|
-
import {
|
|
118
|
+
import { readWorkflowExecutionList } from "./queries.js";
|
|
123
119
|
import { workflowExecutionRunTarget } from "./run-target.js";
|
|
124
120
|
import { workflowExecutionSearchExtractor } from "./search-extractor.js";
|
|
125
121
|
import type { SandboxLane } from "../../sandbox/lane.js";
|
|
@@ -484,60 +480,48 @@ async function get(
|
|
|
484
480
|
}
|
|
485
481
|
|
|
486
482
|
/**
|
|
487
|
-
* List — list.go:
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
* request `org` narrows when non-blank (the Java
|
|
493
|
-
* WorkflowExecutionListHandler baseline: blank = permission-bounded
|
|
494
|
-
* across orgs); no scope = the full scan with `org` a deliberate no-op
|
|
495
|
-
* on this single-tenant edition — byte-identical. The scope runs AFTER
|
|
496
|
-
* the phase and criteria filters (stigmer-cloud 20260913.04 T02: the
|
|
497
|
-
* scope is the last per-row predicate), so a composed driver is asked
|
|
498
|
-
* about the rows the request keeps, not the kind.
|
|
483
|
+
* List — list.go: the request's org through the index (every org when
|
|
484
|
+
* blank), the legacy top-level phase filter (only when filter.phases is
|
|
485
|
+
* absent), structured filter criteria (T13), the read scope last (census
|
|
486
|
+
* lane 6; no scope = every matching execution). Newest created first and
|
|
487
|
+
* paged; another sort field sorts the whole set (queries.ts).
|
|
499
488
|
*/
|
|
500
489
|
async function list(
|
|
501
490
|
deps: WorkflowExecutionControllerDeps,
|
|
502
491
|
req: ListWorkflowExecutionsRequest,
|
|
503
492
|
identity: CallerIdentity,
|
|
504
493
|
): Promise<WorkflowExecutionList> {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if (req.filter === undefined || req.filter.phases.length === 0) {
|
|
512
|
-
executions = applyLegacyPhaseFilter(executions, req.phase);
|
|
513
|
-
}
|
|
514
|
-
executions = applyFilterCriteria(executions, req.filter);
|
|
515
|
-
executions = await restrictListByReadScope(
|
|
516
|
-
deps.listReadScope,
|
|
494
|
+
const legacyPhase =
|
|
495
|
+
req.filter === undefined || req.filter.phases.length === 0;
|
|
496
|
+
return readWorkflowExecutionList({
|
|
497
|
+
store: deps.store,
|
|
498
|
+
logger: deps.logger,
|
|
499
|
+
listReadScope: deps.listReadScope,
|
|
517
500
|
identity,
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
req.
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
501
|
+
query: { org: req.org },
|
|
502
|
+
request: req,
|
|
503
|
+
sortField: req.sortField,
|
|
504
|
+
sortAscending: req.sortAscending,
|
|
505
|
+
keep: (execution) =>
|
|
506
|
+
(!legacyPhase || matchesLegacyPhase(execution, req.phase)) &&
|
|
507
|
+
matchesFilterCriteria(execution, req.filter),
|
|
508
|
+
fingerprint: {
|
|
509
|
+
lane: "workflowExecution.list",
|
|
510
|
+
org: req.org,
|
|
511
|
+
phase: req.phase,
|
|
512
|
+
tags: req.tags,
|
|
513
|
+
filter: req.filter,
|
|
514
|
+
sortField: req.sortField,
|
|
515
|
+
sortAscending: req.sortAscending,
|
|
516
|
+
},
|
|
534
517
|
});
|
|
535
518
|
}
|
|
536
519
|
|
|
537
520
|
/**
|
|
538
521
|
* ListByWorkflow — list_by_workflow.go: the request field accepts either
|
|
539
|
-
* a Workflow ID or a WorkflowInstance ID, so both
|
|
540
|
-
*
|
|
522
|
+
* a Workflow ID or a WorkflowInstance ID, so both of the index's keys are
|
|
523
|
+
* read. Same filter, scope and order as list (census lane 7; bounded by
|
|
524
|
+
* the workflow id, org never consulted).
|
|
541
525
|
*/
|
|
542
526
|
async function listByWorkflow(
|
|
543
527
|
deps: WorkflowExecutionControllerDeps,
|
|
@@ -547,39 +531,27 @@ async function listByWorkflow(
|
|
|
547
531
|
if (req.workflowId === "") {
|
|
548
532
|
throw invalidArgumentError("workflow_id is required");
|
|
549
533
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const all = await loadAllWorkflowExecutions(
|
|
555
|
-
deps.store,
|
|
556
|
-
deps.logger,
|
|
557
|
-
"failed to list workflow executions",
|
|
558
|
-
);
|
|
559
|
-
let executions = all.filter(
|
|
560
|
-
(execution) =>
|
|
561
|
-
execution.spec?.workflowInstanceId === req.workflowId ||
|
|
562
|
-
execution.spec?.workflowId === req.workflowId,
|
|
563
|
-
);
|
|
564
|
-
executions = applyFilterCriteria(executions, req.filter);
|
|
565
|
-
executions = await restrictListByReadScope(
|
|
566
|
-
deps.listReadScope,
|
|
534
|
+
return readWorkflowExecutionList({
|
|
535
|
+
store: deps.store,
|
|
536
|
+
logger: deps.logger,
|
|
537
|
+
listReadScope: deps.listReadScope,
|
|
567
538
|
identity,
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
539
|
+
query: {
|
|
540
|
+
anyKey: [
|
|
541
|
+
{ name: "workflow", value: req.workflowId },
|
|
542
|
+
{ name: "workflow_instance", value: req.workflowId },
|
|
543
|
+
],
|
|
544
|
+
},
|
|
545
|
+
request: req,
|
|
546
|
+
sortField: req.sortField,
|
|
547
|
+
sortAscending: req.sortAscending,
|
|
548
|
+
keep: (execution) => matchesFilterCriteria(execution, req.filter),
|
|
549
|
+
fingerprint: {
|
|
550
|
+
lane: "workflowExecution.listByWorkflow",
|
|
551
|
+
workflowId: req.workflowId,
|
|
552
|
+
filter: req.filter,
|
|
553
|
+
sortField: req.sortField,
|
|
554
|
+
sortAscending: req.sortAscending,
|
|
555
|
+
},
|
|
584
556
|
});
|
|
585
557
|
}
|
|
@@ -63,6 +63,14 @@ export function applyFilterCriteria(
|
|
|
63
63
|
return executions.filter((execution) => matchesFilter(execution, filter));
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** The same criteria for one execution, for a page read's per-row filter. */
|
|
67
|
+
export function matchesFilterCriteria(
|
|
68
|
+
execution: WorkflowExecution,
|
|
69
|
+
filter: ExecutionFilterCriteria | undefined,
|
|
70
|
+
): boolean {
|
|
71
|
+
return filter === undefined || matchesFilter(execution, filter);
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
function matchesFilter(
|
|
67
75
|
execution: WorkflowExecution,
|
|
68
76
|
f: ExecutionFilterCriteria,
|
|
@@ -244,12 +252,17 @@ export function applyLegacyPhaseFilter(
|
|
|
244
252
|
executions: WorkflowExecution[],
|
|
245
253
|
phase: ExecutionPhase,
|
|
246
254
|
): WorkflowExecution[] {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
255
|
+
return executions.filter((execution) => matchesLegacyPhase(execution, phase));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The legacy phase filter for one execution. */
|
|
259
|
+
export function matchesLegacyPhase(
|
|
260
|
+
execution: WorkflowExecution,
|
|
261
|
+
phase: ExecutionPhase,
|
|
262
|
+
): boolean {
|
|
263
|
+
return (
|
|
264
|
+
phase === ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED ||
|
|
265
|
+
(execution.status?.phase ?? ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED) ===
|
|
266
|
+
phase
|
|
254
267
|
);
|
|
255
268
|
}
|
|
@@ -10,16 +10,15 @@
|
|
|
10
10
|
* deliberately absent when nothing completed (an average over nothing is
|
|
11
11
|
* not 0).
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* proto default instance
|
|
20
|
-
* (success_rate 0, NO cost summary) falls out of the scoping, never
|
|
21
|
-
* special case. No scope composed = the
|
|
22
|
-
* (the request's org deliberately not consulted — single-user semantics).
|
|
13
|
+
* The rows are the requested org's, created within the time window, read
|
|
14
|
+
* through the list index (queries.ts) — every edition honours the org, so
|
|
15
|
+
* a member of several organizations never sees every org's numbers on
|
|
16
|
+
* every dashboard (the Java GetExecutionSummary baseline). With a composed
|
|
17
|
+
* ListReadScope those rows are offered to its restrict verb — exact, one
|
|
18
|
+
* read of the kind — and when the caller may see NONE of them the answer
|
|
19
|
+
* is the proto default instance: the conformance-pinned multi-tenant zero
|
|
20
|
+
* shape (success_rate 0, NO cost summary) falls out of the scoping, never
|
|
21
|
+
* a special case. No scope composed = every row of the org and window.
|
|
23
22
|
*
|
|
24
23
|
* Tie order in the two ranked lists is not wire-stable in Go (map
|
|
25
24
|
* iteration feeds a stable sort), so ties here — deterministic first-seen
|
|
@@ -50,11 +49,13 @@ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/
|
|
|
50
49
|
|
|
51
50
|
import type { Logger } from "../../boot/logger.js";
|
|
52
51
|
import type { ListReadScope } from "../../extensions/list-read-scope.js";
|
|
52
|
+
import { restrictListByReadScope } from "../../extensions/list-read-scope.js";
|
|
53
53
|
import type { CallerIdentity } from "../../extensions/identity.js";
|
|
54
54
|
import type { Store } from "../../store/interface.js";
|
|
55
|
+
import { listIndexInstantOfMillis } from "../../store/list-index.js";
|
|
55
56
|
|
|
56
57
|
import { parseRfc3339Ms } from "./execution-filter.js";
|
|
57
|
-
import {
|
|
58
|
+
import { loadWorkflowExecutions } from "./queries.js";
|
|
58
59
|
|
|
59
60
|
const RANK_LIMIT = 10;
|
|
60
61
|
|
|
@@ -70,30 +71,36 @@ export async function getExecutionSummary(
|
|
|
70
71
|
req: GetExecutionSummaryRequest,
|
|
71
72
|
identity: CallerIdentity,
|
|
72
73
|
): Promise<ExecutionSummary> {
|
|
73
|
-
|
|
74
|
+
// The store's window is one millisecond wider than the loop's check
|
|
75
|
+
// below, which stays the authority: it only ever narrows the read.
|
|
76
|
+
const cutoffMs = resolveTimeCutoffMs(req.timeWindow);
|
|
77
|
+
let executions = await loadWorkflowExecutions(
|
|
74
78
|
deps.store,
|
|
75
79
|
deps.logger,
|
|
80
|
+
{
|
|
81
|
+
org: req.org,
|
|
82
|
+
...(cutoffMs === undefined
|
|
83
|
+
? {}
|
|
84
|
+
: { createdAtOrAfter: listIndexInstantOfMillis(cutoffMs - 1) }),
|
|
85
|
+
},
|
|
76
86
|
"failed to list workflow executions for summary",
|
|
77
87
|
);
|
|
78
88
|
|
|
79
89
|
if (deps.listReadScope !== undefined) {
|
|
80
|
-
|
|
90
|
+
executions = await restrictListByReadScope(
|
|
91
|
+
deps.listReadScope,
|
|
81
92
|
identity,
|
|
82
93
|
ApiResourceKind.workflow_execution,
|
|
94
|
+
executions,
|
|
95
|
+
"",
|
|
83
96
|
);
|
|
84
|
-
if (
|
|
97
|
+
if (executions.length === 0) {
|
|
85
98
|
// The Java baseline's empty arm: the default instance, before any
|
|
86
99
|
// aggregation — success_rate 0 and NO cost summary by construction.
|
|
87
100
|
return create(ExecutionSummarySchema);
|
|
88
101
|
}
|
|
89
|
-
executions = executions.filter(
|
|
90
|
-
(execution) =>
|
|
91
|
-
authorizedIds.has(execution.metadata?.id ?? "") &&
|
|
92
|
-
execution.metadata?.org === req.org,
|
|
93
|
-
);
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
const cutoffMs = resolveTimeCutoffMs(req.timeWindow);
|
|
97
104
|
const workflowFilter = req.workflowId;
|
|
98
105
|
|
|
99
106
|
const phaseCounts: Record<number, number> = {};
|
|
@@ -203,9 +210,7 @@ export async function getExecutionSummary(
|
|
|
203
210
|
for (const durationMs of completedDurationsMs) {
|
|
204
211
|
totalMs += durationMs;
|
|
205
212
|
}
|
|
206
|
-
summary.avgDuration = durationFromMs(
|
|
207
|
-
totalMs / completedDurationsMs.length,
|
|
208
|
-
);
|
|
213
|
+
summary.avgDuration = durationFromMs(totalMs / completedDurationsMs.length);
|
|
209
214
|
}
|
|
210
215
|
|
|
211
216
|
summary.topFailingWorkflows = buildFailureRanks(
|
|
@@ -255,7 +260,9 @@ function isActivePhase(phase: ExecutionPhase): boolean {
|
|
|
255
260
|
}
|
|
256
261
|
|
|
257
262
|
/** Go auditCreatedAt: nil timestamp → zero time (undefined here). */
|
|
258
|
-
function auditCreatedAtMs(
|
|
263
|
+
function auditCreatedAtMs(
|
|
264
|
+
createdAt: Timestamp | undefined,
|
|
265
|
+
): number | undefined {
|
|
259
266
|
if (createdAt === undefined) {
|
|
260
267
|
return undefined;
|
|
261
268
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workflow-execution list index (store/list-index.ts): a run names
|
|
3
|
+
* its workflow and, when it ran through one, its instance;
|
|
4
|
+
* `listByWorkflow` accepts either id, so it reads both keys at once.
|
|
5
|
+
*
|
|
6
|
+
* A change to `keys` bumps `revision` (boot/__tests__/list-indexes.test.ts
|
|
7
|
+
* pins the pair).
|
|
8
|
+
*/
|
|
9
|
+
import { WorkflowExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/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 workflowExecutionListIndex = declareListIndex({
|
|
15
|
+
kind: ApiResourceKind.workflow_execution,
|
|
16
|
+
schema: WorkflowExecutionSchema,
|
|
17
|
+
revision: 1,
|
|
18
|
+
keys: {
|
|
19
|
+
workflow: field("spec.workflow_id"),
|
|
20
|
+
workflow_instance: field("spec.workflow_instance_id"),
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ListPendingApprovals — ports list_pending_approvals.go (T14 dashboard):
|
|
3
|
-
*
|
|
4
|
-
* and projects them into PendingApproval
|
|
5
|
-
*
|
|
3
|
+
* reads the org's IN_PROGRESS executions for tasks in
|
|
4
|
+
* WORKFLOW_TASK_WAITING_APPROVAL and projects them into PendingApproval
|
|
5
|
+
* entries, newest execution first. total_count is the whole set's; the
|
|
6
|
+
* entries are one page (default 20, at most 100), continued by a token
|
|
7
|
+
* whose position is an execution's list-index cursor plus the approval's
|
|
8
|
+
* place among its waiting tasks — stable while approvals land and resolve
|
|
9
|
+
* between pages, where an offset would skip or repeat them.
|
|
6
10
|
*/
|
|
7
11
|
import { create } from "@bufbuild/protobuf";
|
|
8
12
|
import type { Timestamp } from "@bufbuild/protobuf/wkt";
|
|
@@ -29,14 +33,25 @@ import type { Logger } from "../../boot/logger.js";
|
|
|
29
33
|
import type { CallerIdentity } from "../../extensions/identity.js";
|
|
30
34
|
import type { ListReadScope } from "../../extensions/list-read-scope.js";
|
|
31
35
|
import { restrictListByReadScope } from "../../extensions/list-read-scope.js";
|
|
36
|
+
import { invalidArgumentError } from "../../pipeline/errors.js";
|
|
37
|
+
import {
|
|
38
|
+
decodeListPageToken,
|
|
39
|
+
encodeListPageToken,
|
|
40
|
+
listPageFingerprint,
|
|
41
|
+
} from "../../pipeline/steps/list-page.js";
|
|
42
|
+
import type { ListPageTokenCursor } from "../../pipeline/steps/list-page.js";
|
|
32
43
|
import type { Store } from "../../store/interface.js";
|
|
44
|
+
import {
|
|
45
|
+
compareListIndexOrder,
|
|
46
|
+
listIndexInstant,
|
|
47
|
+
} from "../../store/list-index.js";
|
|
33
48
|
|
|
34
49
|
import {
|
|
35
50
|
DEFAULT_PENDING_APPROVALS_PAGE_SIZE,
|
|
36
51
|
MAX_PENDING_APPROVALS_PAGE_SIZE,
|
|
37
52
|
} from "./constants.js";
|
|
38
53
|
import { parseRfc3339Ms } from "./execution-filter.js";
|
|
39
|
-
import {
|
|
54
|
+
import { loadWorkflowExecutions } from "./queries.js";
|
|
40
55
|
|
|
41
56
|
export interface PendingApprovalsDeps {
|
|
42
57
|
readonly store: Store;
|
|
@@ -50,44 +65,68 @@ export async function listPendingApprovals(
|
|
|
50
65
|
req: ListPendingApprovalsRequest,
|
|
51
66
|
identity: CallerIdentity,
|
|
52
67
|
): Promise<PendingApprovalsList> {
|
|
53
|
-
// Census lane 8
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
// Census lane 8: the org's executions that CAN carry a
|
|
69
|
+
// pending approval (in progress, a task waiting) through the list index,
|
|
70
|
+
// then the scope — the last per-row predicate, so a composed driver is
|
|
71
|
+
// asked about a handful of rows — before the approvals projection and
|
|
72
|
+
// its pagination, the Java WorkflowExecutionListPendingApprovalsHandler
|
|
73
|
+
// order.
|
|
74
|
+
if (req.pageSize < 0) {
|
|
75
|
+
throw invalidArgumentError("page_size must not be negative");
|
|
76
|
+
}
|
|
77
|
+
const fingerprint = listPageFingerprint({
|
|
78
|
+
lane: "workflowExecution.listPendingApprovals",
|
|
79
|
+
org: req.org,
|
|
80
|
+
});
|
|
81
|
+
const after =
|
|
82
|
+
req.pageToken === ""
|
|
83
|
+
? undefined
|
|
84
|
+
: decodeListPageToken(req.pageToken, fingerprint);
|
|
85
|
+
|
|
61
86
|
const executions = await restrictListByReadScope(
|
|
62
87
|
deps.listReadScope,
|
|
63
88
|
identity,
|
|
64
89
|
ApiResourceKind.workflow_execution,
|
|
65
90
|
(
|
|
66
|
-
await
|
|
91
|
+
await loadWorkflowExecutions(
|
|
67
92
|
deps.store,
|
|
68
93
|
deps.logger,
|
|
94
|
+
{ org: req.org },
|
|
69
95
|
"failed to list workflow executions for pending approvals",
|
|
70
96
|
)
|
|
71
97
|
).filter(mayHavePendingApproval),
|
|
72
|
-
|
|
98
|
+
"",
|
|
73
99
|
);
|
|
74
100
|
|
|
75
101
|
let pageSize = req.pageSize;
|
|
76
|
-
if (pageSize
|
|
102
|
+
if (pageSize === 0) {
|
|
77
103
|
pageSize = DEFAULT_PENDING_APPROVALS_PAGE_SIZE;
|
|
78
104
|
}
|
|
79
105
|
if (pageSize > MAX_PENDING_APPROVALS_PAGE_SIZE) {
|
|
80
106
|
pageSize = MAX_PENDING_APPROVALS_PAGE_SIZE;
|
|
81
107
|
}
|
|
82
108
|
|
|
83
|
-
|
|
109
|
+
// Newest execution first (the index's order), an execution's waiting
|
|
110
|
+
// tasks in their order; each approval's position is its execution's
|
|
111
|
+
// cursor and its place among that execution's waiting tasks.
|
|
112
|
+
const approvals: Array<{
|
|
113
|
+
readonly approval: PendingApproval;
|
|
114
|
+
readonly at: ListPageTokenCursor & { readonly position: number };
|
|
115
|
+
}> = [];
|
|
84
116
|
for (const execution of executions) {
|
|
117
|
+
const cursor = {
|
|
118
|
+
createdAt: listIndexInstant(
|
|
119
|
+
execution.status?.audit?.specAudit?.createdAt,
|
|
120
|
+
),
|
|
121
|
+
id: execution.metadata?.id ?? "",
|
|
122
|
+
};
|
|
123
|
+
let position = 0;
|
|
85
124
|
for (const task of execution.status?.tasks ?? []) {
|
|
86
125
|
if (task.status !== WorkflowTaskStatus.WORKFLOW_TASK_WAITING_APPROVAL) {
|
|
87
126
|
continue;
|
|
88
127
|
}
|
|
89
|
-
approvals.push(
|
|
90
|
-
create(PendingApprovalSchema, {
|
|
128
|
+
approvals.push({
|
|
129
|
+
approval: create(PendingApprovalSchema, {
|
|
91
130
|
executionId: execution.metadata?.id ?? "",
|
|
92
131
|
workflowName: execution.metadata?.name ?? "",
|
|
93
132
|
// task_name, not task_id: the composite task_id ("gate:2")
|
|
@@ -98,21 +137,39 @@ export async function listPendingApprovals(
|
|
|
98
137
|
requestedAt: parseTimestampString(task.startedAt),
|
|
99
138
|
uiHint: task.uiHint,
|
|
100
139
|
}),
|
|
101
|
-
|
|
140
|
+
at: { cursor, position },
|
|
141
|
+
});
|
|
142
|
+
position += 1;
|
|
102
143
|
}
|
|
103
144
|
}
|
|
104
145
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
146
|
+
const start =
|
|
147
|
+
after === undefined
|
|
148
|
+
? 0
|
|
149
|
+
: approvals.findIndex((a) => comesAfter(a.at, after));
|
|
150
|
+
const remaining = start < 0 ? [] : approvals.slice(start);
|
|
151
|
+
const page = remaining.slice(0, pageSize);
|
|
152
|
+
const last = page[page.length - 1];
|
|
109
153
|
|
|
110
154
|
return create(PendingApprovalsListSchema, {
|
|
111
|
-
entries:
|
|
112
|
-
totalCount,
|
|
155
|
+
entries: page.map((a) => a.approval),
|
|
156
|
+
totalCount: approvals.length,
|
|
157
|
+
nextPageToken:
|
|
158
|
+
remaining.length > pageSize && last !== undefined
|
|
159
|
+
? encodeListPageToken(last.at.cursor, fingerprint, last.at.position)
|
|
160
|
+
: "",
|
|
113
161
|
});
|
|
114
162
|
}
|
|
115
163
|
|
|
164
|
+
/** Whether an approval comes strictly after a token's position. */
|
|
165
|
+
function comesAfter(
|
|
166
|
+
at: ListPageTokenCursor & { readonly position: number },
|
|
167
|
+
after: ListPageTokenCursor,
|
|
168
|
+
): boolean {
|
|
169
|
+
const order = compareListIndexOrder(at.cursor, after.cursor);
|
|
170
|
+
return order > 0 || (order === 0 && at.position > (after.position ?? -1));
|
|
171
|
+
}
|
|
172
|
+
|
|
116
173
|
/** Go parseTimestampString: RFC3339 or nothing (nil on parse failure). */
|
|
117
174
|
function parseTimestampString(value: string): Timestamp | undefined {
|
|
118
175
|
const ms = parseRfc3339Ms(value);
|