@pikku/mongodb 0.12.5 → 0.12.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,82 @@
1
1
  # @pikku/mongodb
2
2
 
3
+ ## 0.12.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 92cd5b1: feat(workflow): workflow-owned step retries + stable invocationId
8
+
9
+ The workflow — not the queue — now owns step retry policy, and each step
10
+ invocation gets a stable idempotency key.
11
+ - **Default `retries: 5` with exponential backoff.** A step with no `retries`
12
+ previously inherited the queue's bare default (e.g. pg-boss `retry_limit 2`,
13
+ no backoff) so retries fired instantly and couldn't outlast a transient
14
+ outage. Retries now default to 5 with backoff, resolved at the workflow layer.
15
+ - **`retries: 0` is honored.** Dispatch previously passed `undefined` options
16
+ for `retries: 0`, letting the queue re-run a non-idempotent step up to its own
17
+ default. The resolved policy now always sets `attempts` (`retries: 0` →
18
+ `attempts: 1`), so the queue never second-guesses the workflow. The persisted
19
+ step retries and the dispatched `attempts` are resolved together so
20
+ "retries exhausted" and "no more redeliveries" are the same event.
21
+ - **`workflowStep.invocationId`** — a deterministic, dependency-free
22
+ `uuidv5(runId:stepName)` handed to every step. Unlike `stepId` (minted per
23
+ attempt), it is identical across retries, so a step can dedupe on it
24
+ (`ON CONFLICT (invocationId)`, Stripe idempotency keys, etc.).
25
+ - **queue-bullmq**: `mapPikkuJobToBull` now maps `backoff` (previously dropped,
26
+ so a step's backoff silently never applied on Redis), and `registerQueues`
27
+ throws a clear error when no logger is available (matching queue-pg-boss).
28
+ - **Dispatch failures are recoverable, not fatal.** A step is now marked
29
+ `scheduled` only _after_ it is successfully handed to its transport (queue or
30
+ scheduler) — a failed hand-off leaves it `pending` so a replay re-dispatches
31
+ it, instead of stranding it in `scheduled` (replay would pause forever on a
32
+ job that was never enqueued). A transport outage (e.g. pg-boss momentarily
33
+ down) is surfaced as a new `WorkflowDispatchException`, which the orchestrator
34
+ treats as transient: the run is left running and the orchestrator job is
35
+ rethrown for redelivery (it replays idempotently from the snapshot) rather
36
+ than the whole run being marked `failed`. The orchestrator job now also
37
+ carries its own retry policy, so this holds even when the orchestrator queue
38
+ is configured `retry_limit 0`. A genuine step error still fails the run.
39
+ - **Same step name can be invoked multiple times in one run.** Step rows are now
40
+ keyed per _invocation_: the Nth reach of a step name in a replay resolves to a
41
+ physical key (`name` for the first, `name#N` for repeats), so a literal
42
+ duplicate name no longer clobbers the earlier step's state. The first reach
43
+ keeps the bare name, so existing rows, graph-node matching and `invocationId`s
44
+ are unchanged. Ordinals are derived deterministically from DSL execution order
45
+ and reset each replay.
46
+ - **Step provenance (`fromStepName`) + graph cycles.** Every step now records
47
+ the predecessor it was scheduled from (`fromStepName`; entry steps have none),
48
+ persisted on the step row across all stores (in-memory, kysely, redis,
49
+ mongodb, cloudflare DO) and carried in the queued payload. The DSL wire
50
+ exposes the derived `fromInvocationId` (`uuidv5(runId:fromStepName)`) so
51
+ consumers get the stable predecessor key without a second persisted id —
52
+ `fromStepName` is the source of truth (it is replay-deterministic; `stepId`,
53
+ minted per row, is not). This makes the walked path reconstructable even when
54
+ a node is reached more than once: in `a → b → a → c` the second `a` is a
55
+ distinct ordinal instance (`a#1`) whose `fromStepName` is `b`.
56
+ The graph runner now supports **cycles**: a forward edge into an
57
+ already-started node still collapses to a single run (joins/diamonds are
58
+ unchanged), but a _back-edge_ — one whose target can reach its source — fires
59
+ a fresh ordinal instance, so a node can loop back to itself. Termination is
60
+ the graph's responsibility (branch routing must converge); the engine enforces
61
+ no visit cap.
62
+
63
+ - Updated dependencies [92cd5b1]
64
+ - @pikku/core@0.12.38
65
+
66
+ ## 0.12.6
67
+
68
+ ### Patch Changes
69
+
70
+ - 4b5c75b: feat(auth-js): wire OIDC config (issuer/tenantId) as variables, expand provider registry
71
+ - Move `issuer` and `tenantId` out of the secret blob for OIDC providers (auth0, okta, azure-ad, keycloak, cognito, microsoft-entra-id) — they are public config URLs, not secrets. Now registered via `wireVariable` and loaded at runtime via `services.variables.get()`.
72
+ - Expand provider registry from 13 to 31 providers: reddit, notion, instagram, zoom, figma, tiktok, threads, patreon, dropbox, bitbucket, hubspot, salesforce, atlassian, strava, keycloak, cognito, microsoft-entra-id added.
73
+ - `serialize-auth-gen` emits `wireVariable({...})` declarations and `services.variables.get()` calls in the generated factory for OIDC providers.
74
+ - Integration verifier exercises real `/auth/providers` endpoint with `LocalSecretService` + `LocalVariablesService`, including a spy test proving `services.variables.get('AUTH0_ISSUER')` is called at request time.
75
+
76
+ - Updated dependencies [4b5c75b]
77
+ - Updated dependencies [4b5c75b]
78
+ - @pikku/core@0.12.27
79
+
3
80
  ## 0.12.5
4
81
 
5
82
  ### Patch Changes
@@ -8,6 +8,7 @@ export declare class MongoDBAgentRunService implements AgentRunService {
8
8
  constructor(db: Db);
9
9
  listThreads(options?: {
10
10
  agentName?: string;
11
+ resourceId?: string;
11
12
  limit?: number;
12
13
  offset?: number;
13
14
  }): Promise<AIThread[]>;
@@ -10,8 +10,11 @@ export class MongoDBAgentRunService {
10
10
  this.runs = db.collection('ai_run');
11
11
  }
12
12
  async listThreads(options) {
13
- const { agentName, limit = 50, offset = 0 } = options ?? {};
13
+ const { agentName, resourceId, limit = 50, offset = 0 } = options ?? {};
14
14
  let filter = {};
15
+ if (resourceId) {
16
+ filter.resourceId = resourceId;
17
+ }
15
18
  if (agentName) {
16
19
  const threadIds = await this.runs.distinct('threadId', {
17
20
  agentName,
@@ -14,5 +14,8 @@ export declare class MongoDBChannelStore extends ChannelStore {
14
14
  getChannel(channelId: string): Promise<Channel & {
15
15
  pikkuUserId?: string;
16
16
  }>;
17
+ setState(channelId: string, state: unknown): Promise<void>;
18
+ getState(channelId: string): Promise<unknown | undefined>;
19
+ clearState(channelId: string): Promise<void>;
17
20
  close(): Promise<void>;
18
21
  }
@@ -48,5 +48,15 @@ export class MongoDBChannelStore extends ChannelStore {
48
48
  pikkuUserId: row.pikkuUserId ?? undefined,
49
49
  };
50
50
  }
51
+ async setState(channelId, state) {
52
+ await this.channels.updateOne({ _id: channelId }, { $set: { state } });
53
+ }
54
+ async getState(channelId) {
55
+ const row = await this.channels.findOne({ _id: channelId }, { projection: { state: 1 } });
56
+ return row?.state ?? undefined;
57
+ }
58
+ async clearState(channelId) {
59
+ await this.channels.updateOne({ _id: channelId }, { $unset: { state: '' } });
60
+ }
51
61
  async close() { }
52
62
  }
@@ -21,10 +21,10 @@ export declare class MongoDBSecretService implements SecretService {
21
21
  init(): Promise<void>;
22
22
  private logAudit;
23
23
  private getKEK;
24
- getSecret(key: string): Promise<string>;
25
- getSecretJSON<R = {}>(key: string): Promise<R>;
24
+ getSecret<T = string>(key: string): Promise<T>;
26
25
  hasSecret(key: string): Promise<boolean>;
27
- setSecretJSON(key: string, value: unknown): Promise<void>;
26
+ setSecret(key: string, value: unknown): Promise<void>;
28
27
  deleteSecret(key: string): Promise<void>;
28
+ getSecrets<T extends Record<string, unknown> = Record<string, unknown>>(keys: (keyof T & string)[]): Promise<T>;
29
29
  rotateKEK(): Promise<number>;
30
30
  }
@@ -56,17 +56,12 @@ export class MongoDBSecretService {
56
56
  await this.logAudit(key, 'read');
57
57
  return result;
58
58
  }
59
- async getSecretJSON(key) {
60
- const raw = await this.getSecret(key);
61
- return JSON.parse(raw);
62
- }
63
59
  async hasSecret(key) {
64
60
  const count = await this.secrets.countDocuments({ _id: key }, { limit: 1 });
65
61
  return count > 0;
66
62
  }
67
- async setSecretJSON(key, value) {
68
- const plaintext = JSON.stringify(value);
69
- const { ciphertext, wrappedDEK } = await envelopeEncrypt(this.key, plaintext);
63
+ async setSecret(key, value) {
64
+ const { ciphertext, wrappedDEK } = await envelopeEncrypt(this.key, value);
70
65
  const now = new Date();
71
66
  await this.secrets.updateOne({ _id: key }, {
72
67
  $set: {
@@ -86,6 +81,22 @@ export class MongoDBSecretService {
86
81
  await this.secrets.deleteOne({ _id: key });
87
82
  await this.logAudit(key, 'delete');
88
83
  }
84
+ async getSecrets(keys) {
85
+ const rows = await this.secrets
86
+ .find({ _id: { $in: keys } })
87
+ .toArray();
88
+ const out = {};
89
+ for (const row of rows) {
90
+ try {
91
+ const kek = this.getKEK(row.keyVersion);
92
+ out[row._id] = await envelopeDecrypt(kek, row.ciphertext, row.wrappedDek);
93
+ }
94
+ catch {
95
+ // skip secrets that fail to decrypt
96
+ }
97
+ }
98
+ return out;
99
+ }
89
100
  async rotateKEK() {
90
101
  if (!this.previousKey) {
91
102
  throw new Error('No previousKey configured — nothing to rotate from');
@@ -1,5 +1,5 @@
1
1
  import type { SerializedError } from '@pikku/core';
2
- import { PikkuWorkflowService, type WorkflowPlannedStep, type WorkflowRun, type WorkflowRunWire, type StepState, type WorkflowStatus, type WorkflowVersionStatus } from '@pikku/core/workflow';
2
+ import { PikkuWorkflowService, type WorkflowPlannedStep, type WorkflowRun, type WorkflowRunWire, type StepState, type StepStatus, type WorkflowStatus, type WorkflowVersionStatus } from '@pikku/core/workflow';
3
3
  import type { Db } from 'mongodb';
4
4
  export declare class MongoDBWorkflowService extends PikkuWorkflowService {
5
5
  private initialized;
@@ -10,28 +10,28 @@ export declare class MongoDBWorkflowService extends PikkuWorkflowService {
10
10
  private versions;
11
11
  constructor(db: Db);
12
12
  init(): Promise<void>;
13
- createRun(workflowName: string, input: any, inline: boolean, graphHash: string, wire: WorkflowRunWire, options?: {
13
+ protected createRunImpl(workflowName: string, input: any, inline: boolean, graphHash: string, wire: WorkflowRunWire, options?: {
14
14
  deterministic?: boolean;
15
15
  plannedSteps?: WorkflowPlannedStep[];
16
16
  }): Promise<string>;
17
17
  getRun(id: string): Promise<WorkflowRun | null>;
18
- updateRunStatus(id: string, status: WorkflowStatus, output?: any, error?: SerializedError): Promise<void>;
19
- insertStepState(runId: string, stepName: string, rpcName: string | null, data: any, stepOptions?: {
18
+ protected updateRunStatusImpl(id: string, status: WorkflowStatus, output?: any, error?: SerializedError): Promise<void>;
19
+ protected insertStepStateImpl(runId: string, stepName: string, rpcName: string | null, data: any, stepOptions?: {
20
20
  retries?: number;
21
21
  retryDelay?: string | number;
22
- }): Promise<StepState>;
22
+ }, fromStepName?: string): Promise<StepState>;
23
23
  getStepState(runId: string, stepName: string): Promise<StepState>;
24
24
  getRunHistory(runId: string): Promise<Array<StepState & {
25
25
  stepName: string;
26
26
  }>>;
27
- setStepRunning(stepId: string): Promise<void>;
28
- setStepScheduled(stepId: string): Promise<void>;
27
+ protected setStepRunningImpl(stepId: string): Promise<void>;
28
+ protected setStepScheduledImpl(stepId: string): Promise<void>;
29
29
  private insertHistoryRecord;
30
30
  private getTimestampFieldForStatus;
31
- setStepChildRunId(stepId: string, childRunId: string): Promise<void>;
32
- setStepResult(stepId: string, result: any): Promise<void>;
33
- setStepError(stepId: string, error: Error): Promise<void>;
34
- createRetryAttempt(stepId: string, status: 'pending' | 'running'): Promise<StepState>;
31
+ protected setStepChildRunIdImpl(stepId: string, childRunId: string): Promise<void>;
32
+ protected setStepResultImpl(stepId: string, result: any): Promise<void>;
33
+ protected setStepErrorImpl(stepId: string, error: Error): Promise<void>;
34
+ protected createRetryAttemptImpl(stepId: string, status: 'pending' | 'running'): Promise<StepState>;
35
35
  withRunLock<T>(id: string, fn: () => Promise<T>): Promise<T>;
36
36
  withStepLock<T>(_runId: string, _stepName: string, fn: () => Promise<T>): Promise<T>;
37
37
  getCompletedGraphState(runId: string): Promise<{
@@ -40,12 +40,17 @@ export declare class MongoDBWorkflowService extends PikkuWorkflowService {
40
40
  branchKeys: Record<string, string>;
41
41
  }>;
42
42
  getNodesWithoutSteps(runId: string, nodeIds: string[]): Promise<string[]>;
43
+ getStepInstances(runId: string): Promise<Array<{
44
+ stepName: string;
45
+ status: StepStatus;
46
+ fromStepName?: string;
47
+ }>>;
43
48
  getNodeResults(runId: string, nodeIds: string[]): Promise<Record<string, any>>;
44
- setBranchTaken(stepId: string, branchKey: string): Promise<void>;
45
- updateRunState(runId: string, name: string, value: unknown): Promise<void>;
49
+ protected setBranchTakenImpl(stepId: string, branchKey: string): Promise<void>;
50
+ protected updateRunStateImpl(runId: string, name: string, value: unknown): Promise<void>;
46
51
  getRunState(runId: string): Promise<Record<string, unknown>>;
47
- upsertWorkflowVersion(name: string, graphHash: string, graph: any, source: string, status?: WorkflowVersionStatus): Promise<void>;
48
- updateWorkflowVersionStatus(name: string, graphHash: string, status: WorkflowVersionStatus): Promise<void>;
52
+ protected upsertWorkflowVersionImpl(name: string, graphHash: string, graph: any, source: string, status?: WorkflowVersionStatus): Promise<void>;
53
+ protected updateWorkflowVersionStatusImpl(name: string, graphHash: string, status: WorkflowVersionStatus): Promise<void>;
49
54
  getWorkflowVersion(name: string, graphHash: string): Promise<{
50
55
  graph: any;
51
56
  source: string;
@@ -28,7 +28,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
28
28
  await this.versions.createIndex({ workflowName: 1, graphHash: 1 }, { unique: true });
29
29
  this.initialized = true;
30
30
  }
31
- async createRun(workflowName, input, inline, graphHash, wire, options) {
31
+ async createRunImpl(workflowName, input, inline, graphHash, wire, options) {
32
32
  const id = crypto.randomUUID();
33
33
  const now = new Date();
34
34
  await this.runs.insertOne({
@@ -52,7 +52,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
52
52
  async getRun(id) {
53
53
  return this.runService.getRun(id);
54
54
  }
55
- async updateRunStatus(id, status, output, error) {
55
+ async updateRunStatusImpl(id, status, output, error) {
56
56
  await this.runs.updateOne({ _id: id }, {
57
57
  $set: {
58
58
  status,
@@ -62,7 +62,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
62
62
  },
63
63
  });
64
64
  }
65
- async insertStepState(runId, stepName, rpcName, data, stepOptions) {
65
+ async insertStepStateImpl(runId, stepName, rpcName, data, stepOptions, fromStepName) {
66
66
  const stepId = crypto.randomUUID();
67
67
  const now = new Date();
68
68
  await this.steps.insertOne({
@@ -77,6 +77,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
77
77
  branchTaken: null,
78
78
  retries: stepOptions?.retries ?? null,
79
79
  retryDelay: stepOptions?.retryDelay?.toString() ?? null,
80
+ fromStepName: fromStepName ?? null,
80
81
  createdAt: now,
81
82
  updatedAt: now,
82
83
  });
@@ -89,6 +90,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
89
90
  attemptCount: 1,
90
91
  retries: stepOptions?.retries,
91
92
  retryDelay: stepOptions?.retryDelay?.toString(),
93
+ fromStepName,
92
94
  createdAt: now,
93
95
  updatedAt: now,
94
96
  };
@@ -112,6 +114,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
112
114
  attemptCount,
113
115
  retries: row.retries != null ? Number(row.retries) : undefined,
114
116
  retryDelay: row.retryDelay ?? undefined,
117
+ fromStepName: row.fromStepName ?? undefined,
115
118
  createdAt: new Date(row.createdAt),
116
119
  updatedAt: new Date(row.updatedAt),
117
120
  };
@@ -119,7 +122,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
119
122
  async getRunHistory(runId) {
120
123
  return this.runService.getRunHistory(runId);
121
124
  }
122
- async setStepRunning(stepId) {
125
+ async setStepRunningImpl(stepId) {
123
126
  await this.steps.updateOne({ _id: stepId }, { $set: { status: 'running', updatedAt: new Date() } });
124
127
  const latestHistory = await this.stepHistory
125
128
  .find({ workflowStepId: stepId })
@@ -130,7 +133,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
130
133
  await this.stepHistory.updateOne({ _id: latestHistory[0]._id }, { $set: { status: 'running' } });
131
134
  }
132
135
  }
133
- async setStepScheduled(stepId) {
136
+ async setStepScheduledImpl(stepId) {
134
137
  await this.steps.updateOne({ _id: stepId }, { $set: { status: 'scheduled', updatedAt: new Date() } });
135
138
  }
136
139
  async insertHistoryRecord(stepId, status, result, error) {
@@ -167,7 +170,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
167
170
  return 'createdAt';
168
171
  }
169
172
  }
170
- async setStepChildRunId(stepId, childRunId) {
173
+ async setStepChildRunIdImpl(stepId, childRunId) {
171
174
  await this.steps.updateOne({ _id: stepId }, {
172
175
  $set: {
173
176
  childRunId,
@@ -175,7 +178,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
175
178
  },
176
179
  });
177
180
  }
178
- async setStepResult(stepId, result) {
181
+ async setStepResultImpl(stepId, result) {
179
182
  await this.steps.updateOne({ _id: stepId }, {
180
183
  $set: {
181
184
  status: 'succeeded',
@@ -193,7 +196,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
193
196
  await this.stepHistory.updateOne({ _id: latestHistory[0]._id }, { $set: { status: 'succeeded', result } });
194
197
  }
195
198
  }
196
- async setStepError(stepId, error) {
199
+ async setStepErrorImpl(stepId, error) {
197
200
  const serializedError = {
198
201
  message: error.message,
199
202
  stack: error.stack,
@@ -216,7 +219,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
216
219
  await this.stepHistory.updateOne({ _id: latestHistory[0]._id }, { $set: { status: 'failed', error: serializedError } });
217
220
  }
218
221
  }
219
- async createRetryAttempt(stepId, status) {
222
+ async createRetryAttemptImpl(stepId, status) {
220
223
  await this.steps.updateOne({ _id: stepId }, {
221
224
  $set: {
222
225
  status,
@@ -240,6 +243,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
240
243
  attemptCount,
241
244
  retries: row.retries != null ? Number(row.retries) : undefined,
242
245
  retryDelay: row.retryDelay ?? undefined,
246
+ fromStepName: row.fromStepName ?? undefined,
243
247
  createdAt: new Date(row.createdAt),
244
248
  updatedAt: new Date(row.updatedAt),
245
249
  };
@@ -293,6 +297,17 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
293
297
  const existingStepNames = new Set(result.map((r) => r.stepName));
294
298
  return nodeIds.filter((id) => !existingStepNames.has(id));
295
299
  }
300
+ async getStepInstances(runId) {
301
+ const rows = await this.steps
302
+ .find({ workflowRunId: runId })
303
+ .project({ stepName: 1, status: 1, fromStepName: 1 })
304
+ .toArray();
305
+ return rows.map((r) => ({
306
+ stepName: r.stepName,
307
+ status: r.status,
308
+ fromStepName: r.fromStepName ?? undefined,
309
+ }));
310
+ }
296
311
  async getNodeResults(runId, nodeIds) {
297
312
  if (nodeIds.length === 0)
298
313
  return {};
@@ -309,10 +324,10 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
309
324
  }
310
325
  return results;
311
326
  }
312
- async setBranchTaken(stepId, branchKey) {
327
+ async setBranchTakenImpl(stepId, branchKey) {
313
328
  await this.steps.updateOne({ _id: stepId }, { $set: { branchTaken: branchKey, updatedAt: new Date() } });
314
329
  }
315
- async updateRunState(runId, name, value) {
330
+ async updateRunStateImpl(runId, name, value) {
316
331
  if (!/^[a-zA-Z0-9_]+$/.test(name)) {
317
332
  throw new Error('Invalid state key name');
318
333
  }
@@ -329,7 +344,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
329
344
  return {};
330
345
  return row.state ?? {};
331
346
  }
332
- async upsertWorkflowVersion(name, graphHash, graph, source, status) {
347
+ async upsertWorkflowVersionImpl(name, graphHash, graph, source, status) {
333
348
  await this.versions.updateOne({ workflowName: name, graphHash }, {
334
349
  $setOnInsert: {
335
350
  workflowName: name,
@@ -341,7 +356,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
341
356
  },
342
357
  }, { upsert: true });
343
358
  }
344
- async updateWorkflowVersionStatus(name, graphHash, status) {
359
+ async updateWorkflowVersionStatusImpl(name, graphHash, status) {
345
360
  await this.versions.updateOne({ workflowName: name, graphHash }, { $set: { status } });
346
361
  }
347
362
  async getWorkflowVersion(name, graphHash) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/mongodb",
3
- "version": "0.12.5",
3
+ "version": "0.12.7",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "module": "dist/src/index.js",
@@ -17,7 +17,7 @@
17
17
  "prepublishOnly": "yarn build"
18
18
  },
19
19
  "peerDependencies": {
20
- "@pikku/core": "^0.12.19"
20
+ "@pikku/core": "^0.12.38"
21
21
  },
22
22
  "dependencies": {
23
23
  "mongodb": "^6.12.0"
@@ -26,5 +26,8 @@
26
26
  "mongodb": "^6.12.0",
27
27
  "mongodb-memory-server": "^10.4.0",
28
28
  "typescript": "^5.9"
29
+ },
30
+ "exports": {
31
+ ".": "./dist/src/index.js"
29
32
  }
30
33
  }
@@ -70,13 +70,18 @@ export class MongoDBAgentRunService implements AgentRunService {
70
70
 
71
71
  async listThreads(options?: {
72
72
  agentName?: string
73
+ resourceId?: string
73
74
  limit?: number
74
75
  offset?: number
75
76
  }): Promise<AIThread[]> {
76
- const { agentName, limit = 50, offset = 0 } = options ?? {}
77
+ const { agentName, resourceId, limit = 50, offset = 0 } = options ?? {}
77
78
 
78
79
  let filter: Record<string, any> = {}
79
80
 
81
+ if (resourceId) {
82
+ filter.resourceId = resourceId
83
+ }
84
+
80
85
  if (agentName) {
81
86
  const threadIds = await this.runs.distinct('threadId', {
82
87
  agentName,
@@ -8,6 +8,7 @@ interface ChannelDoc {
8
8
  createdAt: Date
9
9
  openingData: any
10
10
  pikkuUserId: string | null
11
+ state?: unknown
11
12
  lastWire: Date
12
13
  }
13
14
 
@@ -88,5 +89,21 @@ export class MongoDBChannelStore extends ChannelStore {
88
89
  }
89
90
  }
90
91
 
92
+ public async setState(channelId: string, state: unknown): Promise<void> {
93
+ await this.channels.updateOne({ _id: channelId }, { $set: { state } })
94
+ }
95
+
96
+ public async getState(channelId: string): Promise<unknown | undefined> {
97
+ const row = await this.channels.findOne(
98
+ { _id: channelId },
99
+ { projection: { state: 1 } }
100
+ )
101
+ return row?.state ?? undefined
102
+ }
103
+
104
+ public async clearState(channelId: string): Promise<void> {
105
+ await this.channels.updateOne({ _id: channelId }, { $unset: { state: '' } })
106
+ }
107
+
91
108
  public async close(): Promise<void> {}
92
109
  }
@@ -87,36 +87,23 @@ export class MongoDBSecretService implements SecretService {
87
87
  throw new Error(`No KEK available for key_version ${version}`)
88
88
  }
89
89
 
90
- async getSecret(key: string): Promise<string> {
90
+ async getSecret<T = string>(key: string): Promise<T> {
91
91
  const row = await this.secrets.findOne({ _id: key })
92
92
  if (!row) throw new Error('Requested secret not found')
93
93
 
94
94
  const kek = this.getKEK(row.keyVersion)
95
- const result = await envelopeDecrypt<string>(
96
- kek,
97
- row.ciphertext,
98
- row.wrappedDek
99
- )
95
+ const result = await envelopeDecrypt<T>(kek, row.ciphertext, row.wrappedDek)
100
96
  await this.logAudit(key, 'read')
101
97
  return result
102
98
  }
103
99
 
104
- async getSecretJSON<R = {}>(key: string): Promise<R> {
105
- const raw = await this.getSecret(key)
106
- return JSON.parse(raw) as R
107
- }
108
-
109
100
  async hasSecret(key: string): Promise<boolean> {
110
101
  const count = await this.secrets.countDocuments({ _id: key }, { limit: 1 })
111
102
  return count > 0
112
103
  }
113
104
 
114
- async setSecretJSON(key: string, value: unknown): Promise<void> {
115
- const plaintext = JSON.stringify(value)
116
- const { ciphertext, wrappedDEK } = await envelopeEncrypt(
117
- this.key,
118
- plaintext
119
- )
105
+ async setSecret(key: string, value: unknown): Promise<void> {
106
+ const { ciphertext, wrappedDEK } = await envelopeEncrypt(this.key, value)
120
107
  const now = new Date()
121
108
 
122
109
  await this.secrets.updateOne(
@@ -144,6 +131,28 @@ export class MongoDBSecretService implements SecretService {
144
131
  await this.logAudit(key, 'delete')
145
132
  }
146
133
 
134
+ async getSecrets<
135
+ T extends Record<string, unknown> = Record<string, unknown>,
136
+ >(keys: (keyof T & string)[]): Promise<T> {
137
+ const rows = await this.secrets
138
+ .find({ _id: { $in: keys } } as any)
139
+ .toArray()
140
+ const out: Record<string, unknown> = {}
141
+ for (const row of rows) {
142
+ try {
143
+ const kek = this.getKEK(row.keyVersion)
144
+ out[row._id] = await envelopeDecrypt(
145
+ kek,
146
+ row.ciphertext,
147
+ row.wrappedDek
148
+ )
149
+ } catch {
150
+ // skip secrets that fail to decrypt
151
+ }
152
+ }
153
+ return out as T
154
+ }
155
+
147
156
  async rotateKEK(): Promise<number> {
148
157
  if (!this.previousKey) {
149
158
  throw new Error('No previousKey configured — nothing to rotate from')
@@ -71,7 +71,7 @@ function registerTests(name: string, getDb: () => Db) {
71
71
  auditReads: true,
72
72
  })
73
73
  await service.init()
74
- await service.setSecretJSON('audit-test', 'value')
74
+ await service.setSecret('audit-test', 'value')
75
75
  await service.getSecret('audit-test')
76
76
  await service.deleteSecret('audit-test')
77
77
 
@@ -94,7 +94,7 @@ function registerTests(name: string, getDb: () => Db) {
94
94
  auditReads: false,
95
95
  })
96
96
  await service.init()
97
- await service.setSecretJSON('no-read-audit', 'value')
97
+ await service.setSecret('no-read-audit', 'value')
98
98
  await service.getSecret('no-read-audit')
99
99
 
100
100
  const logs = await getDb()
@@ -5,6 +5,7 @@ import {
5
5
  type WorkflowRun,
6
6
  type WorkflowRunWire,
7
7
  type StepState,
8
+ type StepStatus,
8
9
  type WorkflowStatus,
9
10
  type WorkflowVersionStatus,
10
11
  } from '@pikku/core/workflow'
@@ -41,6 +42,7 @@ interface WorkflowStepDoc {
41
42
  branchTaken: string | null
42
43
  retries: number | null
43
44
  retryDelay: string | null
45
+ fromStepName?: string | null
44
46
  createdAt: Date
45
47
  updatedAt: Date
46
48
  }
@@ -110,7 +112,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
110
112
  this.initialized = true
111
113
  }
112
114
 
113
- async createRun(
115
+ protected async createRunImpl(
114
116
  workflowName: string,
115
117
  input: any,
116
118
  inline: boolean,
@@ -148,7 +150,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
148
150
  return this.runService.getRun(id)
149
151
  }
150
152
 
151
- async updateRunStatus(
153
+ protected async updateRunStatusImpl(
152
154
  id: string,
153
155
  status: WorkflowStatus,
154
156
  output?: any,
@@ -167,12 +169,13 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
167
169
  )
168
170
  }
169
171
 
170
- async insertStepState(
172
+ protected async insertStepStateImpl(
171
173
  runId: string,
172
174
  stepName: string,
173
175
  rpcName: string | null,
174
176
  data: any,
175
- stepOptions?: { retries?: number; retryDelay?: string | number }
177
+ stepOptions?: { retries?: number; retryDelay?: string | number },
178
+ fromStepName?: string
176
179
  ): Promise<StepState> {
177
180
  const stepId = crypto.randomUUID()
178
181
  const now = new Date()
@@ -189,6 +192,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
189
192
  branchTaken: null,
190
193
  retries: stepOptions?.retries ?? null,
191
194
  retryDelay: stepOptions?.retryDelay?.toString() ?? null,
195
+ fromStepName: fromStepName ?? null,
192
196
  createdAt: now,
193
197
  updatedAt: now,
194
198
  })
@@ -203,6 +207,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
203
207
  attemptCount: 1,
204
208
  retries: stepOptions?.retries,
205
209
  retryDelay: stepOptions?.retryDelay?.toString(),
210
+ fromStepName,
206
211
  createdAt: now,
207
212
  updatedAt: now,
208
213
  }
@@ -232,6 +237,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
232
237
  attemptCount,
233
238
  retries: row.retries != null ? Number(row.retries) : undefined,
234
239
  retryDelay: row.retryDelay ?? undefined,
240
+ fromStepName: row.fromStepName ?? undefined,
235
241
  createdAt: new Date(row.createdAt),
236
242
  updatedAt: new Date(row.updatedAt),
237
243
  }
@@ -243,7 +249,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
243
249
  return this.runService.getRunHistory(runId)
244
250
  }
245
251
 
246
- async setStepRunning(stepId: string): Promise<void> {
252
+ protected async setStepRunningImpl(stepId: string): Promise<void> {
247
253
  await this.steps.updateOne(
248
254
  { _id: stepId },
249
255
  { $set: { status: 'running', updatedAt: new Date() } }
@@ -263,7 +269,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
263
269
  }
264
270
  }
265
271
 
266
- async setStepScheduled(stepId: string): Promise<void> {
272
+ protected async setStepScheduledImpl(stepId: string): Promise<void> {
267
273
  await this.steps.updateOne(
268
274
  { _id: stepId },
269
275
  { $set: { status: 'scheduled', updatedAt: new Date() } }
@@ -313,7 +319,10 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
313
319
  }
314
320
  }
315
321
 
316
- async setStepChildRunId(stepId: string, childRunId: string): Promise<void> {
322
+ protected async setStepChildRunIdImpl(
323
+ stepId: string,
324
+ childRunId: string
325
+ ): Promise<void> {
317
326
  await this.steps.updateOne(
318
327
  { _id: stepId },
319
328
  {
@@ -325,7 +334,10 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
325
334
  )
326
335
  }
327
336
 
328
- async setStepResult(stepId: string, result: any): Promise<void> {
337
+ protected async setStepResultImpl(
338
+ stepId: string,
339
+ result: any
340
+ ): Promise<void> {
329
341
  await this.steps.updateOne(
330
342
  { _id: stepId },
331
343
  {
@@ -352,7 +364,10 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
352
364
  }
353
365
  }
354
366
 
355
- async setStepError(stepId: string, error: Error): Promise<void> {
367
+ protected async setStepErrorImpl(
368
+ stepId: string,
369
+ error: Error
370
+ ): Promise<void> {
356
371
  const serializedError: SerializedError = {
357
372
  message: error.message,
358
373
  stack: error.stack,
@@ -385,7 +400,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
385
400
  }
386
401
  }
387
402
 
388
- async createRetryAttempt(
403
+ protected async createRetryAttemptImpl(
389
404
  stepId: string,
390
405
  status: 'pending' | 'running'
391
406
  ): Promise<StepState> {
@@ -418,6 +433,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
418
433
  attemptCount,
419
434
  retries: row.retries != null ? Number(row.retries) : undefined,
420
435
  retryDelay: row.retryDelay ?? undefined,
436
+ fromStepName: row.fromStepName ?? undefined,
421
437
  createdAt: new Date(row.createdAt),
422
438
  updatedAt: new Date(row.updatedAt),
423
439
  }
@@ -491,6 +507,20 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
491
507
  return nodeIds.filter((id) => !existingStepNames.has(id))
492
508
  }
493
509
 
510
+ async getStepInstances(runId: string): Promise<
511
+ Array<{ stepName: string; status: StepStatus; fromStepName?: string }>
512
+ > {
513
+ const rows = await this.steps
514
+ .find({ workflowRunId: runId })
515
+ .project({ stepName: 1, status: 1, fromStepName: 1 })
516
+ .toArray()
517
+ return rows.map((r: any) => ({
518
+ stepName: r.stepName,
519
+ status: r.status as StepStatus,
520
+ fromStepName: r.fromStepName ?? undefined,
521
+ }))
522
+ }
523
+
494
524
  async getNodeResults(
495
525
  runId: string,
496
526
  nodeIds: string[]
@@ -512,14 +542,17 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
512
542
  return results
513
543
  }
514
544
 
515
- async setBranchTaken(stepId: string, branchKey: string): Promise<void> {
545
+ protected async setBranchTakenImpl(
546
+ stepId: string,
547
+ branchKey: string
548
+ ): Promise<void> {
516
549
  await this.steps.updateOne(
517
550
  { _id: stepId },
518
551
  { $set: { branchTaken: branchKey, updatedAt: new Date() } }
519
552
  )
520
553
  }
521
554
 
522
- async updateRunState(
555
+ protected async updateRunStateImpl(
523
556
  runId: string,
524
557
  name: string,
525
558
  value: unknown
@@ -547,7 +580,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
547
580
  return row.state ?? {}
548
581
  }
549
582
 
550
- async upsertWorkflowVersion(
583
+ protected async upsertWorkflowVersionImpl(
551
584
  name: string,
552
585
  graphHash: string,
553
586
  graph: any,
@@ -570,7 +603,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
570
603
  )
571
604
  }
572
605
 
573
- async updateWorkflowVersionStatus(
606
+ protected async updateWorkflowVersionStatusImpl(
574
607
  name: string,
575
608
  graphHash: string,
576
609
  status: WorkflowVersionStatus