@pikku/mongodb 0.12.23 → 0.12.25

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.
@@ -1,4 +1,4 @@
1
- import { buildRemoteHeaders } from '@pikku/core/remote';
1
+ import { buildMeshHeaders } from '@pikku/core/mesh';
2
2
  import { getAllFunctionNames } from '@pikku/core/function';
3
3
  export class MongoDBDeploymentService {
4
4
  db;
@@ -40,7 +40,10 @@ export class MongoDBDeploymentService {
40
40
  createdAt: now,
41
41
  },
42
42
  }, { upsert: true });
43
- this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), this.heartbeatInterval);
43
+ this.heartbeatTimer = setInterval(() => {
44
+ // sendHeartbeat catches its own errors; it retries on the next tick
45
+ void this.sendHeartbeat();
46
+ }, this.heartbeatInterval);
44
47
  }
45
48
  async stop() {
46
49
  if (this.heartbeatTimer) {
@@ -54,20 +57,20 @@ export class MongoDBDeploymentService {
54
57
  }
55
58
  }
56
59
  async invoke(funcName, data, session, traceId) {
57
- const headers = await buildRemoteHeaders(this.jwt, this.secrets, funcName, session, traceId);
60
+ const headers = await buildMeshHeaders(this.jwt, this.secrets, funcName, session, traceId);
58
61
  const cutoff = new Date(Date.now() - this.heartbeatTtl);
59
62
  const result = await this.deployments.findOne({ functions: funcName, lastHeartbeat: { $gt: cutoff } }, { sort: { lastHeartbeat: -1 } });
60
63
  if (!result) {
61
64
  throw new Error(`No deployment found for function '${funcName}'`);
62
65
  }
63
- const url = `${result.endpoint}/remote/rpc/${encodeURIComponent(funcName)}`;
66
+ const url = `${result.endpoint}/mesh/rpc/${encodeURIComponent(funcName)}`;
64
67
  const response = await fetch(url, {
65
68
  method: 'POST',
66
69
  headers: { 'Content-Type': 'application/json', ...headers },
67
70
  body: JSON.stringify({ data }),
68
71
  });
69
72
  if (!response.ok) {
70
- throw new Error(`Remote RPC call to '${funcName}' failed: ${response.status}`);
73
+ throw new Error(`Mesh RPC call to '${funcName}' failed: ${response.status}`);
71
74
  }
72
75
  return response.json();
73
76
  }
@@ -35,11 +35,25 @@ export class MongoDBWorkflowRunService {
35
35
  .find({ workflowRunId: runId })
36
36
  .sort({ createdAt: 1 })
37
37
  .toArray();
38
+ const history = await this.stepHistory
39
+ .find({ workflowStepId: { $in: result.map((row) => row._id) } })
40
+ .sort({ createdAt: 1, attempt: 1 })
41
+ .toArray();
42
+ const latest = new Map();
43
+ for (const row of history)
44
+ latest.set(row.workflowStepId, row);
38
45
  return result.map((row) => ({
39
46
  stepId: row._id,
40
47
  stepName: row.stepName,
41
48
  rpcName: row.rpcName ?? undefined,
42
49
  data: row.data ?? undefined,
50
+ childRunId: row.childRunId ?? undefined,
51
+ succeededAt: latest.get(row._id)?.succeededAt
52
+ ? new Date(latest.get(row._id).succeededAt)
53
+ : undefined,
54
+ failedAt: latest.get(row._id)?.failedAt
55
+ ? new Date(latest.get(row._id).failedAt)
56
+ : undefined,
43
57
  status: row.status,
44
58
  result: row.result ?? undefined,
45
59
  error: row.error ?? undefined,
@@ -23,6 +23,11 @@ export declare class MongoDBWorkflowService extends PikkuWorkflowService {
23
23
  }, fromStepName?: string): Promise<StepState>;
24
24
  getStepState(runId: string, stepName: string): Promise<StepState>;
25
25
  private toStepState;
26
+ getRunSteps(runId: string): Promise<Array<StepState & {
27
+ stepName: string;
28
+ rpcName?: string;
29
+ data?: any;
30
+ }>>;
26
31
  getRunHistory(runId: string): Promise<Array<StepState & {
27
32
  stepName: string;
28
33
  }>>;
@@ -1,5 +1,13 @@
1
1
  import { leaseAttemptsExhausted, PikkuWorkflowService, WorkflowStepFunctionMismatchError, WorkflowStepLeaseExpiredError, WorkflowStepSupersededError, } from '@pikku/core/workflow';
2
2
  import { MongoDBWorkflowRunService } from './mongodb-workflow-run-service.js';
3
+ const ENCODED_STATE_KEY = '__enc_';
4
+ /** Field names cannot hold `.` or `$`, and step names are free text. */
5
+ const encodeStateKey = (name) => /^[a-zA-Z0-9_]+$/.test(name) && !name.startsWith(ENCODED_STATE_KEY)
6
+ ? name
7
+ : ENCODED_STATE_KEY + Buffer.from(name, 'utf8').toString('hex');
8
+ const decodeStateKey = (key) => key.startsWith(ENCODED_STATE_KEY)
9
+ ? Buffer.from(key.slice(ENCODED_STATE_KEY.length), 'hex').toString('utf8')
10
+ : key;
3
11
  export class MongoDBWorkflowService extends PikkuWorkflowService {
4
12
  initialized = false;
5
13
  runService;
@@ -123,6 +131,9 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
123
131
  updatedAt: new Date(row.updatedAt),
124
132
  };
125
133
  }
134
+ async getRunSteps(runId) {
135
+ return this.runService.getRunSteps(runId);
136
+ }
126
137
  async getRunHistory(runId) {
127
138
  return this.runService.getRunHistory(runId);
128
139
  }
@@ -412,12 +423,9 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
412
423
  await this.steps.updateOne({ _id: stepId }, { $set: { branchTaken: branchKey, updatedAt: new Date() } });
413
424
  }
414
425
  async updateRunStateImpl(runId, name, value) {
415
- if (!/^[a-zA-Z0-9_]+$/.test(name)) {
416
- throw new Error('Invalid state key name');
417
- }
418
426
  await this.runs.updateOne({ _id: runId }, {
419
427
  $set: {
420
- [`state.${name}`]: value,
428
+ [`state.${encodeStateKey(name)}`]: value,
421
429
  updatedAt: new Date(),
422
430
  },
423
431
  });
@@ -426,7 +434,10 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
426
434
  const row = await this.runs.findOne({ _id: runId }, { projection: { state: 1 } });
427
435
  if (!row)
428
436
  return {};
429
- return row.state ?? {};
437
+ return Object.fromEntries(Object.entries(row.state ?? {}).map(([key, value]) => [
438
+ decodeStateKey(key),
439
+ value,
440
+ ]));
430
441
  }
431
442
  async upsertWorkflowVersionImpl(name, graphHash, graph, source, status) {
432
443
  await this.versions.updateOne({ workflowName: name, graphHash }, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/mongodb",
3
- "version": "0.12.23",
3
+ "version": "0.12.25",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -15,24 +15,30 @@
15
15
  "scripts": {
16
16
  "tsc": "tsc",
17
17
  "ncu": "npx npm-check-updates",
18
- "build": "tsc -b",
18
+ "build": "bun run clean && tsc -b",
19
19
  "test": "bash run-tests.sh",
20
20
  "test:watch": "bash run-tests.sh --watch",
21
21
  "test:coverage": "bash run-tests.sh --coverage",
22
22
  "release": "npm run build && npm test",
23
- "prepublishOnly": "bun run build"
23
+ "prepublishOnly": "bun run build",
24
+ "clean": "rm -rf dist tsconfig.tsbuildinfo"
24
25
  },
25
26
  "peerDependencies": {
26
- "@pikku/core": "^0.12.133"
27
+ "@pikku/core": "^0.12.146"
27
28
  },
28
29
  "dependencies": {
29
- "mongodb": "^7.5.0"
30
+ "mongodb": "^7.7.0"
30
31
  },
31
32
  "devDependencies": {
32
- "mongodb": "^7.5.0",
33
- "mongodb-memory-server": "^11.2.0"
33
+ "mongodb": "^7.7.0",
34
+ "mongodb-memory-server": "^11.3.0"
34
35
  },
35
36
  "exports": {
36
- ".": "./dist/src/index.js"
37
- }
37
+ ".": "./dist/src/index.js",
38
+ "./package.json": "./package.json"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "!dist/**/*.tsbuildinfo"
43
+ ]
38
44
  }
package/CHANGELOG.md DELETED
@@ -1,1069 +0,0 @@
1
- # @pikku/mongodb
2
-
3
- ## 0.12.23
4
-
5
- ### Patch Changes
6
-
7
- - a16b0ba: `RedisWorkflowService` and `MongoDBWorkflowService` now record a step's lease, so a step whose worker died is claimed again once the lease lapses, as it already was on Kysely. Both fence a step's result, error and lease renewal to the attempt that claimed it: a superseded worker's write throws `WorkflowStepSupersededError` and leaves the newer attempt's state alone. Redis does the check and the write in one Lua script. MongoDB now keeps the attempt number on the step document, advances it in the same guarded update that grants the lease, and tags every history document with the attempt it belongs to.
8
- - a16b0ba: The workflow run lease takes the app's `leaseService` (`holdLease(leaseService, 'workflow-run:<id>', …)`) in place of `pg_advisory_lock` / `GET_LOCK`, for DSL and graph runs alike. A message for a run already being orchestrated elsewhere wakes the run again a second later instead of failing, so a long pass never spends the retry budget the queue keeps for real failures. An inline run whose lease was lost after its body finished keeps the outcome it wrote. `PgWorkflowQueueOptions` and `RunLockHoldTimeoutError` are removed. `WorkflowService.withRunLock` is renamed `withRunLease`, since what it holds runs out unless renewed; a custom workflow service overriding it renames the method.
9
-
10
- Pass the lease service to the workflow service — `new PgKyselyWorkflowService(db, { leaseService })` — register the same instance as the app's `leaseService`, and migrate `pikku_lease`.
11
-
12
- A step lease is renewed by the same loop as the run lease. A graph node whose worker died is dispatched again once its lease lapses, instead of leaving the run waiting on it. A replayed step on Kysely now sees its lease, as `getStepState` does. The in-memory store refuses a superseded worker's outcome, as the database stores do. The MySQL workflow service can now record a workflow version; it used `ON CONFLICT`, which MySQL does not have.
13
-
14
- `pikku db generate` now writes the `pikku_lease` table for any project that runs workflows, not only one that reaches `leaseService`, since every persistent workflow service holds its runs and steps there.
15
-
16
- - a16b0ba: Persistent workflow services take a required `leaseService` (`WorkflowServiceOptions`) and lock runs and steps on it. The Postgres advisory and MySQL `GET_LOCK` step locks, and Redis's own `SET NX` run and step locks, are removed; `RedisWorkflowService` now takes `(connection, { leaseService, keyPrefix? })`.
17
- - Updated dependencies [a16b0ba]
18
- - Updated dependencies [21e9c3a]
19
- - Updated dependencies [a16b0ba]
20
- - Updated dependencies [a16b0ba]
21
- - @pikku/core@0.12.133
22
-
23
- ## 0.12.22
24
-
25
- ### Patch Changes
26
-
27
- - 4c7a1b5: Run the monorepo's own scripts through bun instead of yarn. What moves is the
28
- package manager each package's `prepublishOnly` and build scripts invoke, plus
29
- the two manifest fixes bun needs to resolve the tree: `uWebSockets.js` is
30
- declared with an explicit `github:` specifier, and `@pikku/uws-handler` marks
31
- its `uWebSockets.js` peer optional so a bun install of a consumer that brings
32
- its own uWS app does not try to fetch it from the registry.
33
-
34
- Three published behaviours change, all of them cases where an isolated
35
- `node_modules` or bun as the runtime had been papered over by yarn's hoisting:
36
-
37
- - `@pikku/migrator-sql` turns foreign keys on when it opens a sqlite database
38
- through bun. `node:sqlite` enforces them by default and `bun:sqlite` does not,
39
- which silently turned every `ON DELETE CASCADE` into a no-op under
40
- `bunx --bun pikku`.
41
- - `@pikku/cli` resolves a deploy provider against the project being deployed
42
- rather than against wherever the CLI itself is installed, which is what its
43
- own "is not installed" error asks the user to arrange.
44
- - `@pikku/cli` treats a specifier a runtime hands straight back — bun does this
45
- for the modules it implements itself — as not resolved from the project, so
46
- it falls back rather than loading the runtime's own copy.
47
-
48
- - Updated dependencies [4c7a1b5]
49
- - @pikku/core@0.12.108
50
-
51
- ## 0.12.21
52
-
53
- ### Patch Changes
54
-
55
- - 4450b2a: Name the missing key when a secret is not found.
56
-
57
- Every `SecretService` threw a bare `Requested secret not found`. In a deployed
58
- runtime the stack is minified, so the message was the only evidence there was —
59
- and it identified neither the key nor the service. Each implementation now names
60
- the key it looked for; the better-auth middlewares that skip on an absent secret
61
- match the prefix through one shared predicate instead of the whole string.
62
-
63
- - Updated dependencies [4058c3a]
64
- - Updated dependencies [4058c3a]
65
- - Updated dependencies [4058c3a]
66
- - Updated dependencies [4058c3a]
67
- - Updated dependencies [4058c3a]
68
- - Updated dependencies [4058c3a]
69
- - Updated dependencies [114c079]
70
- - Updated dependencies [4450b2a]
71
- - Updated dependencies [4058c3a]
72
- - Updated dependencies [4058c3a]
73
- - @pikku/core@0.12.93
74
-
75
- ## 0.12.20
76
-
77
- ### Patch Changes
78
-
79
- - 3c0012c: Gate console agent-thread reads and deletes on thread ownership, claim MongoDB workflow steps atomically, and reach the deployment fallback from `rpcWithWire`
80
-
81
- `getAgentThreadMessages` and `deleteAgentThread` in the console addon took a
82
- caller-supplied `threadId` straight to storage, while their siblings
83
- `getAgentThreads` and `getAgentThreadRuns` already filtered to what the session
84
- owns. Both now carry an `isThreadOwner` permission: an admin reaches any thread,
85
- everyone else only their own, and a missing thread is refused rather than 404'd
86
- so it is indistinguishable from someone else's.
87
-
88
- `MongoDBWorkflowService` claimed a step by reading its status and then writing
89
- it, under a `withStepLock` that is a pass-through — so two dispatches racing for
90
- the same step could both proceed and run a side-effecting step twice. The claim
91
- is now a single status-guarded update, atomic on one document.
92
-
93
- `rpcWithWire` threw `RPCNotFoundError` for any unresolved namespaced call
94
- instead of falling through to the deployment service, so a namespaced RPC that
95
- `rpc()` would have dispatched remotely failed when called with an explicit wire.
96
-
97
- - Updated dependencies [3c0012c]
98
- - Updated dependencies [05e47cf]
99
- - Updated dependencies [cfd364a]
100
- - Updated dependencies [05e47cf]
101
- - Updated dependencies [05e47cf]
102
- - Updated dependencies [05e47cf]
103
- - @pikku/core@0.12.90
104
-
105
- ## 0.12.19
106
-
107
- ### Patch Changes
108
-
109
- - 266e3bc: One door per name: `@pikku/core/ecosystem/*` and the package root are gone
110
-
111
- `@pikku/core` published every module twice. `ecosystem/http` re-exported
112
- `./http`, `ecosystem/services` re-exported `./services`, and a name was
113
- reachable through either — so every addition had to be made in two places, and a
114
- consumer's import said nothing about what it actually used. The package root was
115
- the same problem at a larger scale: a single barrel of 206 names that no bundler
116
- could take apart, and the one specifier that revealed nothing at all.
117
-
118
- Both are deleted. Every name now lives on the subpath that owns it, and every
119
- import carries that subpath — `@pikku/core/http`, `@pikku/core/services`,
120
- `@pikku/core/errors`, `@pikku/core/types`.
121
-
122
- Deleting the facades meant the raw subpaths had to become a superset of them,
123
- which they were not: the facade tree had accumulated 25 names with no raw home
124
- and about 26 more filed under a different area than the module they came from.
125
- Those names moved to the area that owns them, and three areas were published as
126
- new entry points rather than left on a root that is going away — `./types` (the
127
- shared type surface, the largest single destination), `./state` and
128
- `./classification`.
129
-
130
- `./classification` is one door onto one subject: what a value is and how it must
131
- be handled. Its three halves would each have been an entry point — the brands
132
- and manifest types, the stored-form helpers (`hashToken`, `unsafeAsSealed` and
133
- friends), and `SecretValue` — split by whether a name happens to be a type or a
134
- value, which is the same defect as the facades. The duration and versioned-id
135
- helpers went to `./utils`, which already published, and `PikkuRequest` went to
136
- `./function`: it is the transport-agnostic request base, not an HTTP one — HTTP
137
- has `PikkuHTTPAbstractRequest`, and the only thing outside core that extends
138
- `PikkuRequest` is Azure's timer request.
139
-
140
- `./types` inherited the root barrel's habit before it inherited its contents, so
141
- the names with an owner elsewhere were moved off it. The middleware types and the
142
- five middleware factories — `pikkuMiddleware`, `pikkuMiddlewareFactory`,
143
- `pikkuChannelMiddleware`, `pikkuChannelMiddlewareFactory` and
144
- `pikkuAgentMiddleware`, runtime values on a types entry point — are now
145
- `@pikku/core/middleware`; the function meta types are `@pikku/core/function`;
146
- `SerializedError` is `@pikku/core/errors`; and the generic TypeScript helpers
147
- (`MakeRequired`, `PickRequired`, `PickOptional`, `RequireAtLeastOne`,
148
- `JSONPrimitive`, `JSONValue`) are `@pikku/core/utils`. What is left on `./types`
149
- is the vocabulary the wirings share, which no single module owns.
150
-
151
- `pikku` was itself a root barrel — `export * from '@pikku/core'` — and
152
- now exports only the services it bundles.
153
-
154
- One module survives at the old specifier, and only for the bootstrap:
155
- `packages/cli` is generated by the _published_ CLI pinned in its `build.sh`, and
156
- that CLI still writes a bare `@pikku/core` into the files it generates for the
157
- CLI itself. `bootstrap-compat/root.ts` carries the eight types it names, a test
158
- in core fails if that list grows, and it goes when the pin moves to a CLI
159
- released from this branch. The adapter names the pinned CLI reaches for —
160
- `pikkuState` and `CreateWireServices` — are rewritten to `@pikku/core/state` and
161
- `@pikku/core/types` by the same `build.sh` patch pass, so no second shim is
162
- needed for them.
163
-
164
- A guard test keeps the root shut: it parses imports and rejects a bare
165
- `@pikku/core` rather than grepping for it, because several tests hold a user's
166
- file as a template literal, where `import … from '@pikku/core'` is fixture text
167
- rather than an import this repo makes.
168
-
169
- An agent scaffold a project generated under an older CLI is refreshed rather
170
- than left to fail: `pikku all` already deleted one importing an entry point
171
- `@pikku/core` no longer publishes, and the `#pikku` hub joins that list.
172
- Without it a project that scaffolds the agent endpoint but
173
- declares no agents keeps the old file forever — the generator that would rewrite
174
- it only runs when agents exist, and the file being present is what stops it
175
- being regenerated as missing.
176
-
177
- `pikku new addon` also wrote a tsconfig `paths` map naming only the deleted hub.
178
- An addon's `imports` map points into `dist`, so `paths` is what resolves
179
- `#pikku/<leaf>` for the addon's own source build — it now names the two leaf
180
- patterns, in both the addon and its test harness.
181
-
182
- - 786dae5: Bump every dependency whose latest release is a major across the monorepo, and
183
- port the code the majors broke: `cookie` 2's `parseCookie`/`stringifySetCookie`
184
- API in `@pikku/core` and the three runtime HTTP adapters, and assistant-ui 0.15's
185
- store client in `@pikku/assistant-ui`.
186
- - 6eef0a0: Bump every dependency to its latest compatible minor/patch across the monorepo.
187
- - Updated dependencies [7722ceb]
188
- - Updated dependencies [375c1ff]
189
- - Updated dependencies [02a70cd]
190
- - Updated dependencies [aeef159]
191
- - Updated dependencies [a281de6]
192
- - Updated dependencies [266e3bc]
193
- - Updated dependencies [02a70cd]
194
- - Updated dependencies [786dae5]
195
- - Updated dependencies [6eef0a0]
196
- - Updated dependencies [3561d67]
197
- - Updated dependencies [a91c433]
198
- - Updated dependencies [02a70cd]
199
- - Updated dependencies [9537f74]
200
- - Updated dependencies [2b57ca8]
201
- - Updated dependencies [266e3bc]
202
- - Updated dependencies [9fce0f1]
203
- - Updated dependencies [83683a0]
204
- - Updated dependencies [456c88b]
205
- - Updated dependencies [456c88b]
206
- - Updated dependencies [c127273]
207
- - @pikku/core@0.12.85
208
-
209
- ## 0.12.18
210
-
211
- ### Patch Changes
212
-
213
- - 7406bfe: Rename the agent runtime from `AI*` to `Agent*` (#596)
214
-
215
- `AI` described the model provider, not the thing being named. Every symbol that
216
- belongs to the agent runtime now says `Agent`; the symbols that genuinely wrap a
217
- model provider — `AIEmbeddingService`, `AIProviderOptions`, `AIEmbedParams`,
218
- `AITranscriptionParams`, `AIGenerateImageParams` and their siblings, and the
219
- `@pikku/ai-vercel` / `@pikku/ai-deepinfra` / `@pikku/ai-voice` packages — keep
220
- their names.
221
-
222
- **Wiring**
223
- - `pikkuAIAgent` → `pikkuAgent`, `pikkuAIScorer` → `pikkuAgentScorer`,
224
- `pikkuAIJudge` → `pikkuAgentJudge`
225
- - `CoreAIAgent` → `CoreAgent`, `AIAgentInput` → `AgentInput`, `AIAgentStep` →
226
- `AgentStep`, `AIMessage` → `AgentMessage`, and the rest of the agent types
227
- - `AIAgentRunnerService` → `AgentRunnerService`, `AIStorageService` →
228
- `AgentStorageService`, `AIRunStateService` → `AgentRunStateService`
229
-
230
- **Entry points**
231
-
232
- `@pikku/core/agent` → `@pikku/core/agent`, `@pikku/core/agent-scorer` →
233
- `@pikku/core/agent-scorer`.
234
-
235
- **Queues**
236
-
237
- The scorer queues are now `agent-score-fast` and `agent-score-slow`. Drain the
238
- old `ai-score-fast` / `ai-score-slow` queues before deploying — jobs still
239
- sitting on them when the new workers start will never be picked up.
240
-
241
- **Scaffolds**
242
-
243
- The agent scaffold pikku wrote for your project — `<scaffold>/agent/agent.gen.ts`
244
- and its schemas file — imports `@pikku/core/ai-agent`, which no longer exists. A
245
- scaffold is normally written once and then left alone, so `pikku all` would find
246
- it present and leave the broken import in place. It now deletes an agent scaffold
247
- importing either removed entry point and regenerates it in the same run. Anything
248
- you added to that file goes with it, so move local edits out first.
249
-
250
- **Database**
251
-
252
- The agent tables are renamed: `ai_threads`, `ai_message`, `ai_tool_call`,
253
- `ai_working_memory`, `ai_run` and `ai_run_score` become `agent_threads`,
254
- `agent_message`, `agent_tool_call`, `agent_working_memory`, `agent_run` and
255
- `agent_run_score`, along with their indexes and the `ai_working_memory_pk`
256
- constraint. The same rename applies to the MongoDB collections.
257
-
258
- `ensurePikkuSchema` creates tables it cannot find, so an existing database will
259
- get empty `agent_*` tables and leave the old data stranded in `ai_*`. Rename
260
- them before the first boot on the new version:
261
-
262
- ```sql
263
- ALTER TABLE ai_threads RENAME TO agent_threads;
264
- ALTER TABLE ai_message RENAME TO agent_message;
265
- ALTER TABLE ai_tool_call RENAME TO agent_tool_call;
266
- ALTER TABLE ai_working_memory RENAME TO agent_working_memory;
267
- ALTER TABLE ai_run RENAME TO agent_run;
268
- ALTER TABLE ai_run_score RENAME TO agent_run_score;
269
- ```
270
-
271
- - 6794681: Publish the ecosystem surface as per-area sub-barrels under `@pikku/core/ecosystem/*`, and point generated code, the CLI, the inspector and the runtime adapters at them.
272
-
273
- 348 names that only generated code, the toolchain or a runtime adapter ever imports now have a second home on `@pikku/core/ecosystem/<area>` — one sub-barrel per area, matching how core already publishes its entrypoints, so no single barrel grows without bound and a consumer only pulls in the area it uses.
274
-
275
- This step is additive: every name is still exported from the entrypoint it was published from before, so nothing downstream breaks. Removing them from the app-facing barrels is a later change, and needs a release carrying `./ecosystem/*` first.
276
-
277
- - Updated dependencies [7406bfe]
278
- - Updated dependencies [6794681]
279
- - @pikku/core@0.12.84
280
-
281
- ## 0.12.17
282
-
283
- ### Patch Changes
284
-
285
- - fd9d834: Stop publishing internals that only their own package or file used. The declarations stay; only the entrypoint re-export is removed, so nothing that imported a name from where it is declared is affected.
286
- - Updated dependencies [063f43a]
287
- - Updated dependencies [ce66bf8]
288
- - Updated dependencies [d0307a8]
289
- - Updated dependencies [ce66bf8]
290
- - Updated dependencies [3ad2131]
291
- - Updated dependencies [b930dca]
292
- - Updated dependencies [b95e77d]
293
- - Updated dependencies [fd9d834]
294
- - Updated dependencies [8978fbd]
295
- - @pikku/core@0.12.82
296
-
297
- ## 0.12.16
298
-
299
- ### Patch Changes
300
-
301
- - e110c55: Add runtime scoring for AI agents: `pikkuAIScorer` for heuristic grades and
302
- `pikkuAIJudge` for LLM-judged ones, graded off the request path on two queue
303
- lanes so a slow judge cannot starve the cheap checks. Grades are sampled
304
- deterministically per `(run, scorer)` and persisted to `ai_run_score`.
305
- - Updated dependencies [e110c55]
306
- - Updated dependencies [e110c55]
307
- - Updated dependencies [e110c55]
308
- - Updated dependencies [acc8077]
309
- - Updated dependencies [905f737]
310
- - Updated dependencies [3cc6428]
311
- - Updated dependencies [c524adf]
312
- - Updated dependencies [e110c55]
313
- - @pikku/core@0.12.81
314
-
315
- ## 0.12.15
316
-
317
- ### Patch Changes
318
-
319
- - 6dada45: fix(workflow,ai-agent): make a run's owner, entry node and step function authoritative
320
-
321
- A graph run may only start at a node the graph declared in `meta.entryNodeIds`, and
322
- the generated `POST /workflow/:workflowName/graph/:nodeId` route that let an HTTP
323
- caller pick the entry node is gone. `startNode` stays for `PikkuTriggerService`,
324
- which names a declared entry node anyway.
325
-
326
- `StepState` now records the `rpcName` the workflow dispatched a step with, and the
327
- step claim rejects a queue message naming a different function with
328
- `WorkflowStepFunctionMismatchError` before mutating any status — a step runs under
329
- the run owner's identity and without the `expose` gate, so the message must not
330
- choose what runs.
331
-
332
- `approveStep` takes the caller's session, and the generated status routes and
333
- streams assert the same `assertWorkflowRunOwner` check: a run started through a
334
- session may only be read and approved by that session's user. A run with no
335
- recorded owner (trigger, scheduler, unauthenticated route) has nobody to compare
336
- against and is still gated by the entrypoint's own `auth`/`permissions`.
337
-
338
- `AIRunStateService.resolveApproval` is now a compare-and-swap returning whether
339
- _this_ caller made the claim, and both agent resume paths run a tool only for the
340
- approvals they claimed — concurrent approvals of one tool call no longer all
341
- execute it.
342
-
343
- - Updated dependencies [41c1a95]
344
- - Updated dependencies [ce96383]
345
- - Updated dependencies [7e60867]
346
- - Updated dependencies [f8f1244]
347
- - Updated dependencies [dcf20cb]
348
- - Updated dependencies [6512384]
349
- - Updated dependencies [e3b4c14]
350
- - Updated dependencies [efd0ed1]
351
- - Updated dependencies [cba98fb]
352
- - Updated dependencies [ce96383]
353
- - Updated dependencies [f8f1244]
354
- - Updated dependencies [f8f1244]
355
- - Updated dependencies [6e93a35]
356
- - Updated dependencies [6dada45]
357
- - @pikku/core@0.12.80
358
-
359
- ## 0.12.14
360
-
361
- ### Patch Changes
362
-
363
- - 78b29f0: `SecretService` now returns a `SecretValue<T>` rather than the bare value, so a
364
- vault secret cannot reach a sink by accident.
365
-
366
- `SecretValue` is nominally typed, which means it is not assignable to `string`
367
- (or to any other concretely-typed field). Every sink with a real type — a
368
- database column, an email body, a session payload — rejects it with no lint
369
- rule involved. The sinks typed `any`, `unknown`, or a free generic — the logger,
370
- queue payloads, webhook and email inputs, and a function's own output — are
371
- guarded with `Safe<T>`, which collapses a `SecretValue` found anywhere inside
372
- `T`, however deeply nested, to `never`.
373
-
374
- Unwrap deliberately at the point the secret reaches the wire:
375
-
376
- ```ts
377
- const secret = await secrets.getSecret('BETTER_AUTH_SECRET')
378
- betterAuth({ secret: secret.reveal() })
379
- ```
380
-
381
- Two behaviours cover what types cannot see. Structured serialization redacts —
382
- `JSON.stringify` and node's inspect both yield `[secret]`, so an audit or log
383
- write stays honest without crashing the request. String coercion throws
384
- `SecretCoercionError`, because a template literal is always a leak.
385
-
386
- `AuditLog.write` is guarded the same way as the logger, since an audit event
387
- carries `input` and `metadata` as `unknown` and nominality alone cannot stop a
388
- secret landing in one.
389
-
390
- `.reveal()` is the deliberate escape hatch, and what it hands back is an
391
- ordinary string as far as every sink signature is concerned. **PKU953** closes
392
- that gap: under `pikku all --security` the inspector reports a revealed secret
393
- that flows into a logger, an audit, a queue, an email or a webhook — `console` included.
394
-
395
- This also fixed a real one: `remote-addon-auth.ts` called `String(token)` on an
396
- `unknown` and wrote the result straight into an `Authorization` header.
397
-
398
- - Updated dependencies [62ea4cc]
399
- - Updated dependencies [9dddff8]
400
- - Updated dependencies [78b29f0]
401
- - @pikku/core@0.12.76
402
-
403
- ## 0.12.13
404
-
405
- ### Patch Changes
406
-
407
- - 6a307f0: Fix four latent correctness bugs in the function, RPC and error runtimes, and
408
- remove dead code from the workflow service surface.
409
-
410
- `WorkflowService.getNodesWithoutSteps` is gone. It was declared on the abstract
411
- service and implemented by all five storage backends, and nothing ever called
412
- it — hence the non-core packages in this changeset, which only lose that method.
413
-
414
- **An RPC could execute twice.** `RPCService.invoke`, its addon path, and
415
- `rpcWithWire` each wrapped the _execution_ of a resolved function in a `try`
416
- whose `catch` treated `RPCNotFoundError` as "not found locally" and re-dispatched
417
- the call through `deploymentService`. A nested `rpc.invoke` to a missing name,
418
- raised from inside an already-running function, therefore re-ran that function on
419
- a remote instance after its local side effects had already committed. Resolution
420
- is now separated from execution on all three paths, so only a genuinely
421
- unresolvable name reaches the fallback. What escapes to callers is unchanged.
422
-
423
- **`addonNamespace` leaked between sibling calls.** The function runner's
424
- middleware path restored `rpc`, `functionId`, `audit` and `addonNamespace` after
425
- an invocation; the non-middleware path restored the first three but not the
426
- fourth. A call into an addon function with no middleware left the addon's
427
- namespace on the wire, so subsequent sibling calls resolved the wrong
428
- per-instance singletons and `credentialOverrides`.
429
-
430
- **Errors registered on a subclass never resolved.** `misc.errors` was typed
431
- `Map<PikkuError, ErrorDetails>` — instances — while `addError` stores
432
- constructors, a mismatch hidden by its `error: any` parameter. The instance
433
- lookup in `getErrorResponse` was consequently dead, and lookup fell through to a
434
- scan comparing `constructor.name`, so a subclass of a registered error got no
435
- status mapping at all. The map is now typed `Map<PikkuErrorConstructor, …>` and
436
- lookup walks the prototype chain first. Name matching is retained, deliberately,
437
- as the fallback that keeps error mapping working when two copies of
438
- `@pikku/core` are installed.
439
-
440
- **`createWeakUID` collided across instances.** The prefix was
441
- `Date.now().toString(36)` evaluated at module load, so any two instances loading
442
- the module in the same millisecond emitted identical `channelId` and `requestId`
443
- sequences — reproducibly, not just in principle. It is now seeded lazily from
444
- `crypto.randomUUID()`.
445
-
446
- Also: `pikkuState` keys its global map with `Symbol.for` rather than `Symbol`, so
447
- two copies of the package share registrations instead of silently getting
448
- disjoint state; and the local channel upgrade path no longer keys its middleware
449
- cache on the raw request path, which grew the cache without bound while the
450
- cached value never varied by path.
451
-
452
- - afef587: Close eleven security weaknesses found in a review of `@pikku/core`. Most are
453
- breaking, and two invalidate data or credentials already in the wild — read the
454
- migration notes before upgrading.
455
-
456
- **Breaking: AI agent thread ownership now fails closed.** Reading, listing,
457
- resuming or approving an existing thread or run requires a resolved session
458
- principal (`userId`, or `orgId` for `sessionScope: 'org'`), regardless of the
459
- agent's `auth` setting. Previously a request without a session had no ownership
460
- model at all: the caller-supplied `resourceId` was accepted as the ownership
461
- key, so any caller could read or resume another party's thread by naming its
462
- `resourceId`. Worse, `threadOwnerConstraint` returned `undefined` for a
463
- sessionless caller, and `undefined` means _no filter_ rather than _no rows_ —
464
- so `getAgentThreads` returned every thread in the deployment. It now returns
465
- `string[]`, empty for a sessionless caller, which every storage backend already
466
- treats as matching nothing. Sessionless agents still run one-shot conversations,
467
- each with a fresh unguessable owner; what they lose is cross-request continuity.
468
- Wire a session to restore it.
469
-
470
- **Breaking: stored secrets and credentials must be re-entered.** `deriveKey` ran
471
- a single unsalted round of SHA-256 over the passphrase and used the digest
472
- directly as the AES-GCM key — roughly one hash per brute-force guess, with one
473
- rainbow table working against every deployment. It is now PBKDF2-HMAC-SHA256 at
474
- 600,000 iterations over a random salt. There is no compatibility path: every
475
- value held by the kysely, mongodb and redis secret services and the kysely
476
- credential service becomes undecryptable. They fail loud, naming the key and
477
- `key_version`, so the app hard-fails on first secret read until each is re-set.
478
-
479
- The KEK salt is scoped to the key version and stored alongside it, rather than
480
- per secret, so a bulk read costs one derivation instead of N — `getSecrets` over
481
- 50 secrets went from ~2.3s to ~48ms, and rotation from ~4.6s to ~94ms. This adds
482
- a salt table (kysely), hash field (redis) or collection (mongodb), created
483
- automatically on first use.
484
-
485
- **Breaking: `PIKKU_REMOTE_SECRET` must be at least 32 characters.** The
486
- remote-RPC session envelope moved from PBKDF2 to HKDF, which expands
487
- high-entropy key material rather than stretching a low-entropy passphrase. That
488
- took a remote hop from ~269ms to ~0.4ms — PBKDF2 was running twice per request —
489
- but HKDF supplies no brute-force resistance, so the secret must carry the
490
- entropy itself. A shorter secret now throws `WeakKeyMaterialError` at both ends.
491
- Generate one with `openssl rand -base64 32` and roll it out to every service in
492
- the mesh together: existing bearer tokens are format-incompatible, so a partial
493
- rollout produces 401s until every instance is updated. The Cloudflare, Lambda
494
- and Azure deployment services each hand-rolled a copy of `buildRemoteHeaders`
495
- and now call the shared one, which is what keeps the two sides in step.
496
-
497
- **Breaking: previously signed content URLs stop verifying.** `LocalContent`
498
- signed only `{signedAt, expiresAt, notBefore}`, so a signature proved when a URL
499
- was issued but never what it was issued for — any valid token was a skeleton
500
- key, and swapping the pathname from a public thumbnail to a private document
501
- still verified. The signature now binds the request path. Separately, the
502
- verifier returned "valid" when no JWT service was wired, which is how
503
- `pikku serve` ran: a forged `?signedAt=0&expiresAt=99999999999999` was accepted.
504
- It now rejects with 403, `LocalContent` requires a `JWTService`, and
505
- `pikku serve`/`pikku dev` mint an ephemeral per-process signing key so local
506
- development works without shipping a fail-open path. In-flight signed URLs must
507
- be re-issued.
508
-
509
- **Request body size limits now apply to every adapter.** The `maxBodySize` cap
510
- existed only in `PikkuFetchHTTPRequest`. The real hole was uWebSockets, which
511
- drove `res.onData` itself and concatenated every chunk with no bound and nothing
512
- downstream able to intervene; it now drops chunks past the limit and replies 413
513
- before routing. Fastify delegates to its native `bodyLimit` (set only when
514
- `maxBodySize` is configured, so fastify's stricter 1 MB default is never
515
- loosened), and `PikkuExpressServer` feeds the limit into its body parsers. Two
516
- paths can only reject rather than prevent, and are documented as such:
517
- `express-middleware` mounted on your own app receives an already-parsed body, so
518
- that deployment must bound its own parser; Next server actions bottom out at
519
- `experimental.serverActions.bodySizeLimit`.
520
-
521
- **Breaking: the console addon's privileged functions are gated by default.**
522
- `wireAddon` gains a `scopes` option that applies to every function in the
523
- addon's namespace, and the console scaffold now generates
524
- `wireAddon({ name: 'console', package: '@pikku/addon-console', scopes: ['admin'] })`.
525
- Previously the console's entire privileged surface — around 54 functions
526
- including `credentialGet`, which returns a resolved OAuth token for an arbitrary
527
- `userId`, `updateFunctionBody`, and `installAddon`, which shells out to a
528
- package install — was protected only by an optional host-registered
529
- `addGlobalPermission`. `resolveGlobalPermissions` returns `[]` when none is
530
- registered and permission checking then no-ops, so an app that never registered
531
- one served those functions to anyone, and with the template's default
532
- `scaffold.rpc: "no-auth"` that meant unauthenticated. All of them now return 403
533
- `MissingScopeError` without an `admin`, `admin:*` or `*` scope. **Regenerating
534
- is required** — an app holding an old `console.gen.ts` stays open.
535
- `installAddon` and `installOpenapiAddon` additionally declare their own
536
- `auth: true, scopes: ['admin']`, and `getAgentThreads` now scopes its listing to
537
- the session's own threads unless the caller holds admin.
538
-
539
- Addon scopes are enforced in `runPikkuFunc` rather than at the RPC boundary,
540
- because a wiring can reference an addon function directly — the inspector
541
- records the addon's `packageName` on HTTP, channel, schedule, queue, CLI,
542
- trigger, gateway and MCP wirings — and those paths never call `resolveNamespace`.
543
- Enforcing at the RPC seam would have covered only the `namespace:function` form
544
- while reading as complete.
545
-
546
- **Breaking: `wireAddon`'s `auth` and `tags` now apply on direct wirings too.**
547
- Both were read only by `resolveAddonFunction`, so they had exactly the hole
548
- scopes had: `wireAddon({ name: 'console', package: '@pikku/addon-console', auth:
549
- true, tags: ['admin'] })` gated `rpc('console:credentialGet')` and gated nothing
550
- at all on an HTTP route wired straight to `credentialGet`. A consumer who
551
- reached for the documented way to lock an addon down got a control that was
552
- silently inert on every wiring except one. Both now resolve in `runPikkuFunc`.
553
- `auth` merges as an OR — `auth: false` from an addon is ignored on a direct
554
- wiring, because an addon may require a session the wiring did not but must never
555
- waive one it did. Addon tags resolve to concrete middleware against the **root**
556
- tag groups before the call rather than being folded into the function's
557
- inherited middleware: `addTagMiddleware('admin', …)` is written by the consuming
558
- app and registers under the root package, whereas `combineMiddleware` would look
559
- the tag up under the addon's own `packageName`, where it does not exist.
560
-
561
- One consequence worth naming: an addon that wires `auth: true` and also runs its
562
- own sessionless internal work — a scheduled task or queue worker inside the
563
- addon calling a sibling function — is now gated where it previously was not,
564
- because a bare `rpc('fn')` from inside the addon reaches `runPikkuFunc` with the
565
- addon's `packageName` like any other call. This reverses a decision that
566
- deliberately scoped the gate to the namespaced boundary; that reasoning held
567
- only while the boundary was real, and a direct wiring can enter an addon without
568
- crossing it, so "already inside" is not something the runtime can infer. Such an
569
- addon should carry authorization on the function via
570
- `pikkuFunc({ permissions })`, which has always been enforced on every path. A
571
- follow-up will add execution provenance so an intra-addon call can be
572
- distinguished from an external one and skip the addon-level check; that needs a
573
- marker no caller outside the process can set, which is its own design problem
574
- and does not belong in a security fix.
575
-
576
- **Codegen now warns when an exposed function has no gate (PKU574).** The
577
- generated `POST /rpc/:rpcName` dispatcher forwards to `rpc.exposed`, which
578
- refuses anything without `expose: true` — but nothing checked whether the
579
- target was gated, because a dispatcher cannot know what it dispatches to. The
580
- console shipped ~54 privileged functions through that gap and the toolchain was
581
- silent. The inspector now reports every function that is exposed, sessionless,
582
- and carries no `auth`, `scopes` or `permissions` of its own and none from a
583
- governing `wireAddon`. It is a `warn`, not a critical: `expose: true` on an
584
- ungated sessionless function is correct for a genuinely public endpoint, so it
585
- blocks a build only under `--fail-on-warn`.
586
-
587
- Two pieces of metadata were missing for this to be answerable statically, and
588
- both are now recorded. A `pikkuSessionlessFunc`'s own `auth: true` was read at
589
- runtime but never written to function meta, so a self-gated function was
590
- indistinguishable from an ungated one — `sessionless` carries the baseline, and
591
- `auth` now carries the tightening. And `wireAddon`'s `scopes`, `auth` and `tags`
592
- were not parsed at all: the inspector recorded the addon's `rpcEndpoint` and its
593
- secret, variable and credential overrides, and dropped every one of its gates.
594
- An addon whose gates are not statically knowable is treated as gated, because a
595
- false positive on a correctly-secured addon costs more than the one case it
596
- would catch.
597
-
598
- **Breaking: an application's global permissions now apply inside addons.**
599
- `resolveGlobalPermissions` read only the bucket matching the function's own
600
- package, but the generated `addGlobalPermission` wrapper takes no package
601
- argument and always registers under the root. An app-wide rule like "every
602
- request needs a signed-in user" therefore stopped at the addon boundary, and the
603
- bucket an addon's functions did read was one no host could write to — which is
604
- why the console addon's recommended `addGlobalPermission([isAdmin],
605
- '@pikku/addon-console')` was never a gate anybody could actually install. A
606
- function now resolves the root bucket and its own package's, root first.
607
- Unioning is safe in a way nothing else here would be: globals AND, so adding the
608
- root ones can only tighten. Package buckets stay one-way — a package's globals
609
- never reach root functions, or an installed addon could gate the whole
610
- application. Apps with both a root global and addon-provided functions will see
611
- those functions gated where they previously were not.
612
-
613
- **Codegen now records whether each HTTP route requires a session, and warns
614
- about inert addon tags (PKU575).** Four separate things can demand a session —
615
- the function's `sessionless`, its own `auth`, the route's (or its group's)
616
- `auth`, and the addon it belongs to — and answering "which routes are open?"
617
- meant joining all four by hand and knowing which wins. Each route's meta now
618
- carries the resolved `requiresSession` alongside the route's own `auth`. Scopes
619
- count as requiring one, since they are matched against the session's and fail
620
- closed. Anything not statically knowable resolves to `true`, matching PKU574:
621
- a route that looks stricter than it is costs less than one that looks open and
622
- isn't. Separately, `wireAddon({ tags: ['admin'] })` reads like a gate and is
623
- applied like one right up until no `addTagMiddleware('admin', …)` exists, at
624
- which point it resolves to an empty list and gates nothing; that now warns.
625
- Only addon tags are reported — a tag on a function is as likely to be
626
- organizational, and warning about those would bury the case that matters.
627
-
628
- **Object-shorthand permissions were missing from meta.** The inspector visited
629
- `ts.PropertyAssignment` but not `ts.ShorthandPropertyAssignment`, so
630
- `permissions: { canAdminOrg }` — enforced identically to the longhand form at
631
- runtime, since `verifyPermissions` has a non-array branch — was recorded as _no
632
- permissions at all_. That is the most dangerous direction for meta to be wrong
633
- in: an audit reading it sees an open door where one is shut. It cost this review
634
- a false IDOR report across ~35 billing and org functions before the source
635
- contradicted the metadata.
636
-
637
- **Functions that authorize in their own body can say so.** A webhook receiver
638
- verifying a signature, or a handler redeeming a signed invite, is genuinely
639
- closed while carrying no session, scope or permission — indistinguishable in
640
- meta from one nobody remembered to gate, and so warned about forever by PKU574.
641
- `selfAuthenticated: true` on the function config records the claim and silences
642
- the warning for that function. It is declarative only: nothing at runtime reads
643
- it and it grants nothing. Detection was rejected deliberately — inferring it
644
- from the body means a function that _looks_ like it checks something silences
645
- the warning while checking nothing, and a warning that is usually wrong stops
646
- being read.
647
-
648
- **Breaking: `scaffold.<feature>` is now `boolean | { auth?, path? }`, and `true`
649
- means authenticated.** The old `'auth' | 'no-auth' | false` read like a
650
- starter-file preference while being a live authorization decision, set three
651
- directories from the functions it governed — the shape the console incident
652
- took. A surface now becomes public only by writing `{ auth: false }`, so
653
- omitting a field can never open anything: the failure mode of a forgotten flag
654
- is a locked door. `{ path }` additionally overrides where the file is
655
- generated, which previously could only be set for all features at once via
656
- `pikkuDir`.
657
-
658
- The legacy strings are **refused, not coerced**. `resolveScaffoldFeature` throws
659
- naming the key and its replacement (`"rpc": "no-auth"` → `"rpc": { "auth":
660
- false }`), and it does so at config load, not downstream. An earlier design used
661
- a bare `string` for the output path, under which `"no-auth"` would have parsed
662
- as a file named `no-auth` and every unmigrated config would have silently
663
- produced nonsense; the object form makes any string invalid, so the failure is
664
- loud.
665
-
666
- The collapse is deliberately not uniform in effect. For `rpc` the flag was a
667
- blanket "no anonymous RPC in this app" set on a dispatcher that cannot know what
668
- it dispatches to; for `userAdmin` it was redundant, since the generated
669
- functions are already `pikkuFunc` with `scopes: ['admin:users:list']`. But for
670
- `agent`, `workflow`, `events` and `scenarios` it is the only gate — those
671
- generate real endpoints the app never authors — so `true` keeps them
672
- authenticated rather than opening them as a side effect of a config cleanup.
673
- `webhook` and `remoteRpc` have no auth dimension at all (`serialize-remote-rpc.ts`
674
- hardcodes `auth: false`), so an `{ auth }` on them is ignored. The three
675
- configs in this repo are migrated preserving their current behaviour exactly.
676
-
677
- **Queue job identities are signed.** A job carried the producer's `pikkuUserId`
678
- as a plain string and the worker resolved a session from it with no
679
- verification, so write access to the queue backend was act-as-any-user. The
680
- identity is now `pq1.<claim>.<hmac>`, HMAC-SHA256 over the claim and the
681
- canonicalized job payload, keyed by HKDF expansion of a new
682
- `PIKKU_QUEUE_IDENTITY_SECRET`. Producers opt in by wrapping their queue service
683
- with `SignedQueueService`. This fails safe rather than closed: with no secret
684
- configured the identity is dropped and jobs still process, warning once per
685
- process, so no existing deployment breaks on upgrade — it simply loses queue
686
- identity until the secret is set. The payload rather than the job id is bound
687
- because SQS, Cloudflare Queues, Azure and the in-memory service all mint ids
688
- after `add` returns.
689
-
690
- **Workflow inline state is read from the run record.** `isInline` consulted a
691
- process-local `Map`, while `WorkflowRun.inline` is durable. Any instance that
692
- did not start a run disagreed with the record, so one instance could dispatch a
693
- queued job for a workflow another was already executing in-process. It is now
694
- async and resolves through the durable identity, cached only when a context
695
- already exists so a passive reader allocates nothing. The same `Map` also leaked:
696
- `nextStepKey` fabricated replay state on every step, and `releaseContext`
697
- refused to free anything carrying it, so runs whose steps executed outside a
698
- `beginReplay` bracket — the step-worker queue path — stranded their context and
699
- step state for the process lifetime. Contexts are now released by an explicit
700
- execution counter. Step ordinals reset per execution rather than accumulating
701
- across step-worker invocations in one process, which makes step naming
702
- independent of how work was distributed.
703
-
704
- **Secret reads fail loud in every store.** `MongoDBSecretService.getSecrets`
705
- skipped rows that failed to decrypt, and the redis equivalent dropped every
706
- rejection via `Promise.allSettled`, including the "No KEK available for
707
- key_version N" configuration error. Both now throw, naming the key and its key
708
- version, matching the kysely behaviour. This matters most alongside the KEK
709
- change above: without it, an upgrade surfaces as a partial secrets map and an
710
- opaque downstream failure instead of an error naming the secret to re-enter.
711
-
712
- **A second middleware registration for a pattern no longer erases the first.**
713
- `addHTTPMiddleware`, `addTagMiddleware` and `addChannelMiddleware` groups are
714
- keyed by pattern or tag and held one source file each, so a second file's call
715
- overwrote the first's. Codegen emits its imports from what is stored, so the
716
- losing file was never imported and its middleware never registered — the
717
- runtime composes repeated registrations for a pattern happily, and only codegen
718
- dropped one. Adding an unrelated `addHTTPMiddleware('*', …)` to an app was
719
- enough to silently unregister the generated better-auth session bridge, which
720
- fails open and gives no sign until a request arrives without a session. A group
721
- now carries every registration made for it, and all of them are imported.
722
-
723
- - Updated dependencies [6a307f0]
724
- - Updated dependencies [afef587]
725
- - Updated dependencies [8075f6a]
726
- - @pikku/core@0.12.74
727
-
728
- ## 0.12.12
729
-
730
- ### Patch Changes
731
-
732
- - 8a2c993: Make the workflow service cheaper to run, and fix two ways it lost state.
733
-
734
- The SQL workflow tables had no indexes at all, so every step read, every
735
- history walk and every orchestrator tick was a sequential scan; five indexes
736
- now cover the columns the engine actually queries by. A replay used to ask for
737
- each step's row individually — O(N) reads per replay, O(N^2) over a run — and
738
- now takes one read of the run's steps and serves the walk from it. A step
739
- transition wrote the step row and its history row as two separate statements,
740
- so a crash between them left a step saying `succeeded` whose history still said
741
- `running`; both halves are now one transaction, and the history row is found by
742
- attempt number rather than by sorting on `created_at`, which two attempts can
743
- share. Resolving a dynamic workflow read and parsed every AI-generated workflow
744
- in the deployment to `.find()` one by name; it is a point lookup now.
745
-
746
- Waiting on a run no longer polls at a fixed interval. `pollIntervalMs` became a
747
- ceiling rather than a cadence: polling starts at 10ms and widens towards it, so
748
- a workflow that finishes in milliseconds is no longer held for a full second,
749
- and a long-running one is not read at full rate for its whole life.
750
-
751
- Two backend-specific defects: Redis kept a run's state as one JSON blob and
752
- read-modified-wrote it, so parallel branches setting different variables
753
- overwrote each other — state is a field per variable now, with the old blob
754
- still read underneath so runs in flight keep what they had. Mongo's
755
- `setStepScheduled` never wrote history, leaving a queued step reading as never
756
- dispatched.
757
-
758
- Also: dispatch no longer JSON round-trips every step payload before handing it
759
- to a queue that serialises it anyway — the in-process dev queue, which is the
760
- only one that was relying on it, does it itself now.
761
-
762
- Two more defects. A transition whose step had no live attempt wrote its status
763
- to the step row and silently nothing to history — the exact divergence the
764
- transaction exists to prevent — and now repairs the step and writes the
765
- missing row. And resolving a dynamic workflow was non-deterministic on all
766
- three backends: a name can hold several active versions, and none of them
767
- ordered the candidates, so which one ran could change between two calls
768
- reading identical data. The newest version wins, with the graph hash breaking
769
- a tie.
770
-
771
- The two attempt columns and the five indexes are declared in the workflow
772
- schema, so a fresh database gets them at boot. An existing one gets them from
773
- a migration — `pikku db generate` writes the declaration down — rather than
774
- from DDL issued at boot.
775
-
776
- - a261006: **Breaking:** removed dynamic workflows — runtime-defined workflow graphs stored in the database and resolved by name instead of by codegen.
777
-
778
- The feature was already half-gone. Its authoring surface (`createAgentWorkflow`, `saveAgentWorkflow`, `listAgentWorkflows`, `executeAgentWorkflow`, and the AI-agent instruction builder) was deleted in April 2026 along with its entire e2e suite, and nothing has written a dynamic workflow since. What remained could not execute one either: `executeAgentWorkflow` gated on `pikkuState('workflows', 'meta')`, which only codegen ever populates, so a graph that existed solely in the database was never findable. The two backend families had also drifted onto different `source` sentinels (`'ai-agent'` vs `'dynamic-workflow'`), and the two Redis implementations disagreed on key escaping — so at least one of them matched nothing. Rather than keep shipping plumbing for a path no caller could complete, it is removed until it can be reintroduced deliberately.
779
-
780
- Removed:
781
- - `getAIGeneratedWorkflows` from `WorkflowService` and `WorkflowRunService`, and from every backend (in-memory, Redis, MongoDB, Kysely, and the Cloudflare Durable Object service and client — the last two were already a `return []` stub and a rejection).
782
- - The database-lookup fallbacks in `startWorkflow` and `runWorkflowJob` that resolved a workflow name against stored graphs when static meta had no match.
783
- - `'dynamic-workflow'` from the `WorkflowRuntimeMeta['source']` union.
784
- - `validateWorkflowWiring` and `computeEntryNodeIds` from `@pikku/core/workflow`. These validated AI-authored graphs and had no callers in core; the inspector keeps its own private entry-node computation for static graph wiring, which is unaffected.
785
- - The `workflow-created` AI stream event and its AG-UI `pikku:workflow-created` custom event. Its only emitter went with the April deletion, so it could never fire.
786
- - The console's `console:getAIWorkflows` RPC, the `useAIWorkflows` hook, the "Dynamic" workflow filter and badge, and the trigger-schema scraper that derived an input form from a stored graph's `$ref` bindings.
787
-
788
- Kept, because static graph workflows depend on them and this is not a change to versioning:
789
- - `upsertWorkflowVersion`, `getWorkflowVersion`, `updateWorkflowVersionStatus`, and the `workflowVersions` storage in every backend. These back version-mismatch replay: when a deployed graph's hash changes, in-flight runs continue against the exact graph they started on. No schema migration is needed — the table, its columns, and its `(workflowName, graphHash)` upsert key are unchanged.
790
- - `generateMermaidDiagram`, which renders any workflow graph and is not specific to dynamic ones.
791
-
792
- Static `pikkuWorkflowGraph` and DSL workflows are entirely unaffected: they resolve from codegen'd meta, which was always the only path that worked.
793
-
794
- To revive this post-MVP, the deleted authoring code is recoverable in full — its prompt engineering (a compact tool table upfront, full schemas with flattened dotted output paths returned only after a validation failure) is worth reading before rewriting:
795
-
796
- ```
797
- git show f52f3308b^:packages/core/src/wirings/ai-agent/agent-dynamic-workflow.ts
798
- git show f52f3308b^:packages/core/src/wirings/workflow/graph/graph-validation.ts
799
- git show f52f3308b --stat # the April removal, incl. the three e2e feature files
800
- ```
801
-
802
- Note that reviving it needs more than restoring those files: the queued-step path (`executeWorkflowStep`), `onError` compensation, and sub-workflow resolution all read static meta only and would need a fallback for a graph that exists solely in the database.
803
-
804
- - Updated dependencies [8a2c993]
805
- - Updated dependencies [a261006]
806
- - Updated dependencies [09973b9]
807
- - @pikku/core@0.12.71
808
-
809
- ## 0.12.11
810
-
811
- ### Patch Changes
812
-
813
- - e3d4454: Add job groups, so one shared queue can stay fair without splitting into one
814
- queue per producer.
815
-
816
- A job may now carry `group: { id, tier }`, and a worker may cap how many jobs
817
- of any one group run at once via `groupConcurrency`. On pg-boss this maps to
818
- `localGroupConcurrency`, which excludes at-capacity groups from the fetch query
819
- itself, so a capped group costs nothing rather than being fetched and restored.
820
- BullMQ declares it unsupported (groups are a BullMQ Pro feature) — being
821
- push-based, it can simply use a queue per group at no polling cost.
822
-
823
- Workflow services accept a `queueStrategy`. The default `'per-workflow'` is
824
- unchanged: every workflow gets its own `wf-orchestrator-*` / `wf-step-*` queue,
825
- which is also what lets serverless providers deploy one unit per workflow. The
826
- new `'shared-groups'` routes every workflow through the shared
827
- orchestrator/step-worker queues and isolates them by group instead, so a
828
- monolith runs one set of pollers rather than one per workflow — on a
829
- pull-based backend with dozens of workflows that is the difference between
830
- hundreds of poll loops and twenty. It is for single-process runtimes only; a
831
- per-unit serverless deploy still needs the per-workflow queues to route to its
832
- units.
833
-
834
- - Updated dependencies [24252b8]
835
- - Updated dependencies [e3d4454]
836
- - @pikku/core@0.12.69
837
-
838
- ## 0.12.10
839
-
840
- ### Patch Changes
841
-
842
- - 4324652: Scope AI agent thread reads to the calling session.
843
-
844
- The generated thread-management functions (`getAgentThreads`,
845
- `getAgentThreadMessages`, `getAgentThreadRuns`, `deleteAgentThread`) keyed purely
846
- off a caller-supplied `threadId` and treated `resourceId` as an optional filter,
847
- so omitting it enumerated every tenant's threads.
848
- - `listThreads` gains an `owners` **authorization constraint** (distinct from the
849
- `resourceId` filter): an empty array matches nothing, and it is always derived
850
- from the session, never from input. Implemented across the Kysely, Redis and
851
- MongoDB agent run services, with LIKE/regex metacharacter escaping so an owner
852
- id containing `_` or `%` cannot match a foreign owner.
853
- - The three `threadId`-keyed functions are now guarded by an `isThreadOwner`
854
- `pikkuPermission` rather than an in-body check. A thread that does not exist is
855
- denied rather than 404'd, so it is indistinguishable from one owned by someone
856
- else.
857
- - New `@pikku/core/ai-agent` helpers: `canAccessThread`, `threadOwnerConstraint`,
858
- `sessionPrincipals`, `isOwnedByPrincipal`.
859
-
860
- Services destructured by a wired function are now non-optional inside it.
861
-
862
- The inspector already aggregated the services used by every wired `func`,
863
- `permissions` and `middleware` into `RequiredSingletonServices`, but the
864
- generated function types defaulted their service parameter to the raw `Services`
865
- — so a service declared `foo?: Foo` still arrived as possibly-undefined, forcing
866
- `if (!foo) throw new MissingServiceError(...)` guards that could never fire.
867
- Generated types now expose `WiredSingletonServices` / `WiredServices`
868
- (`RequiredSingletonServices & Services`) and default the `RequiredServices`
869
- generic of functions, permissions, middleware, auth and approval-description
870
- helpers to them. Optionality now means only what it should: "this service may
871
- not be created, because nothing uses it".
872
-
873
- - Updated dependencies [5f19016]
874
- - Updated dependencies [78e4778]
875
- - Updated dependencies [4324652]
876
- - Updated dependencies [de044f8]
877
- - Updated dependencies [cd1a811]
878
- - Updated dependencies [19fa6f0]
879
- - Updated dependencies [b501612]
880
- - Updated dependencies [eb37b1e]
881
- - @pikku/core@0.12.66
882
-
883
- ## 0.12.9
884
-
885
- ### Patch Changes
886
-
887
- - 41ce2cb: Upgrade to TypeScript 6 and raise the minimum Node.js version to 22.
888
-
889
- All packages now build against `typescript@^6.0.3` and declare `engines.node >= 22`. Internal tooling (`ts-json-schema-generator`, `zod-to-ts`) was bumped to TypeScript 6-compatible releases.
890
-
891
- - Updated dependencies [41ce2cb]
892
- - @pikku/core@0.12.44
893
-
894
- ## 0.12.8
895
-
896
- ### Patch Changes
897
-
898
- - 72694f6: feat(workflow): expose per-step attempt count + record running/succeeded/failed timestamps
899
-
900
- `getRunStatus` now returns `attempts` (the latest attempt count) per step, so
901
- consumers can show retry counts without a second history query. It already
902
- computed `duration` from `runningAt`/`succeededAt`, but the kysely and mongodb
903
- workflow stores only stamped those timestamps on the _insert_ path — the
904
- `running` / `succeeded` / `failed` status transitions updated the history row's
905
- status without setting `runningAt` / `succeededAt` / `failedAt`, so `duration`
906
- was always undefined. The transitions now stamp the matching timestamp, so step
907
- duration is populated for kysely- and mongodb-backed runs. (Redis already
908
- stamped on transition.) A shared service-suite test guards both behaviours.
909
-
910
- - Updated dependencies [4be205f]
911
- - Updated dependencies [061c717]
912
- - Updated dependencies [2c55e13]
913
- - Updated dependencies [c745c26]
914
- - Updated dependencies [57900b5]
915
- - Updated dependencies [72694f6]
916
- - @pikku/core@0.12.39
917
-
918
- ## 0.12.7
919
-
920
- ### Patch Changes
921
-
922
- - 92cd5b1: feat(workflow): workflow-owned step retries + stable invocationId
923
-
924
- The workflow — not the queue — now owns step retry policy, and each step
925
- invocation gets a stable idempotency key.
926
- - **Default `retries: 5` with exponential backoff.** A step with no `retries`
927
- previously inherited the queue's bare default (e.g. pg-boss `retry_limit 2`,
928
- no backoff) so retries fired instantly and couldn't outlast a transient
929
- outage. Retries now default to 5 with backoff, resolved at the workflow layer.
930
- - **`retries: 0` is honored.** Dispatch previously passed `undefined` options
931
- for `retries: 0`, letting the queue re-run a non-idempotent step up to its own
932
- default. The resolved policy now always sets `attempts` (`retries: 0` →
933
- `attempts: 1`), so the queue never second-guesses the workflow. The persisted
934
- step retries and the dispatched `attempts` are resolved together so
935
- "retries exhausted" and "no more redeliveries" are the same event.
936
- - **`workflowStep.invocationId`** — a deterministic, dependency-free
937
- `uuidv5(runId:stepName)` handed to every step. Unlike `stepId` (minted per
938
- attempt), it is identical across retries, so a step can dedupe on it
939
- (`ON CONFLICT (invocationId)`, Stripe idempotency keys, etc.).
940
- - **queue-bullmq**: `mapPikkuJobToBull` now maps `backoff` (previously dropped,
941
- so a step's backoff silently never applied on Redis), and `registerQueues`
942
- throws a clear error when no logger is available (matching queue-pg-boss).
943
- - **Dispatch failures are recoverable, not fatal.** A step is now marked
944
- `scheduled` only _after_ it is successfully handed to its transport (queue or
945
- scheduler) — a failed hand-off leaves it `pending` so a replay re-dispatches
946
- it, instead of stranding it in `scheduled` (replay would pause forever on a
947
- job that was never enqueued). A transport outage (e.g. pg-boss momentarily
948
- down) is surfaced as a new `WorkflowDispatchException`, which the orchestrator
949
- treats as transient: the run is left running and the orchestrator job is
950
- rethrown for redelivery (it replays idempotently from the snapshot) rather
951
- than the whole run being marked `failed`. The orchestrator job now also
952
- carries its own retry policy, so this holds even when the orchestrator queue
953
- is configured `retry_limit 0`. A genuine step error still fails the run.
954
- - **Same step name can be invoked multiple times in one run.** Step rows are now
955
- keyed per _invocation_: the Nth reach of a step name in a replay resolves to a
956
- physical key (`name` for the first, `name#N` for repeats), so a literal
957
- duplicate name no longer clobbers the earlier step's state. The first reach
958
- keeps the bare name, so existing rows, graph-node matching and `invocationId`s
959
- are unchanged. Ordinals are derived deterministically from DSL execution order
960
- and reset each replay.
961
- - **Step provenance (`fromStepName`) + graph cycles.** Every step now records
962
- the predecessor it was scheduled from (`fromStepName`; entry steps have none),
963
- persisted on the step row across all stores (in-memory, kysely, redis,
964
- mongodb, cloudflare DO) and carried in the queued payload. The DSL wire
965
- exposes the derived `fromInvocationId` (`uuidv5(runId:fromStepName)`) so
966
- consumers get the stable predecessor key without a second persisted id —
967
- `fromStepName` is the source of truth (it is replay-deterministic; `stepId`,
968
- minted per row, is not). This makes the walked path reconstructable even when
969
- a node is reached more than once: in `a → b → a → c` the second `a` is a
970
- distinct ordinal instance (`a#1`) whose `fromStepName` is `b`.
971
- The graph runner now supports **cycles**: a forward edge into an
972
- already-started node still collapses to a single run (joins/diamonds are
973
- unchanged), but a _back-edge_ — one whose target can reach its source — fires
974
- a fresh ordinal instance, so a node can loop back to itself. Termination is
975
- the graph's responsibility (branch routing must converge); the engine enforces
976
- no visit cap.
977
-
978
- - Updated dependencies [92cd5b1]
979
- - @pikku/core@0.12.38
980
-
981
- ## 0.12.6
982
-
983
- ### Patch Changes
984
-
985
- - 4b5c75b: feat(auth-js): wire OIDC config (issuer/tenantId) as variables, expand provider registry
986
- - 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()`.
987
- - 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.
988
- - `serialize-auth-gen` emits `wireVariable({...})` declarations and `services.variables.get()` calls in the generated factory for OIDC providers.
989
- - 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.
990
-
991
- - Updated dependencies [4b5c75b]
992
- - Updated dependencies [4b5c75b]
993
- - @pikku/core@0.12.27
994
-
995
- ## 0.12.5
996
-
997
- ### Patch Changes
998
-
999
- - b9ed73e: Add deterministic workflow planned-step metadata support and SSE init stream payload generation.
1000
- - Persist `deterministic` and `plannedSteps` on workflow runs in core and service adapters.
1001
- - Expose planned-step metadata on workflow run status responses.
1002
- - Emit an initial `type: 'init'` SSE event for deterministic workflow streams before incremental updates.
1003
- - Add CLI tests covering serialized stream route output for init/update/done event behavior.
1004
-
1005
- - Updated dependencies [b9ed73e]
1006
- - @pikku/core@0.12.19
1007
-
1008
- ## 0.12.4
1009
-
1010
- ### Patch Changes
1011
-
1012
- - 311c0c4: Unify session persistence through SessionStore, remove session blob from ChannelStore
1013
- - PikkuSessionService now persists sessions via SessionStore on set()/clear() instead of every function call
1014
- - ChannelStore no longer stores session data — maps channelId to pikkuUserId only
1015
- - ChannelStore API: setUserSession/getChannelAndSession replaced with setPikkuUserId/getChannel
1016
- - Serverless channel runner resolves sessions from SessionStore using pikkuUserId from ChannelStore
1017
-
1018
- - Updated dependencies [311c0c4]
1019
- - @pikku/core@0.12.18
1020
-
1021
- ## 0.12.3
1022
-
1023
- ### Patch Changes
1024
-
1025
- - a2ee6d0: Replace process.exit(1) with thrown error on MongoDB connection failure to allow graceful error handling.
1026
- - 87433f0: Validate state key names in updateRunState to prevent MongoDB operator injection.
1027
- - Updated dependencies [e412b4d]
1028
- - Updated dependencies [53dc8c8]
1029
- - Updated dependencies [0a1cc51]
1030
- - Updated dependencies [0a1cc51]
1031
- - Updated dependencies [0a1cc51]
1032
- - Updated dependencies [0a1cc51]
1033
- - Updated dependencies [0a1cc51]
1034
- - Updated dependencies [0a1cc51]
1035
- - Updated dependencies [0a1cc51]
1036
- - Updated dependencies [0a1cc51]
1037
- - Updated dependencies [0a1cc51]
1038
- - Updated dependencies [8b9b2e9]
1039
- - Updated dependencies [8b9b2e9]
1040
- - Updated dependencies [b973d44]
1041
- - Updated dependencies [8b9b2e9]
1042
- - Updated dependencies [8b9b2e9]
1043
- - @pikku/core@0.12.9
1044
-
1045
- ## 0.12.2
1046
-
1047
- ### Patch Changes
1048
-
1049
- - 3e79248: Add setStepChildRunId to workflow service implementations and auto-bootstrap in pikku all
1050
- - Updated dependencies [bb27710]
1051
- - Updated dependencies [a31bc63]
1052
- - Updated dependencies [3e79248]
1053
- - Updated dependencies [b0a81cc]
1054
- - Updated dependencies [6413df7]
1055
- - @pikku/core@0.12.6
1056
-
1057
- ## 0.12.1
1058
-
1059
- ### Patch Changes
1060
-
1061
- - 97bab28: Initial release of @pikku/mongodb — MongoDB-backed service implementations for Pikku, including SecretService, ChannelStore, EventHubStore, DeploymentService, WorkflowService, WorkflowRunService, AIStorageService, AIRunStateService, and AgentRunService.
1062
- - c7ff141: Add WorkflowVersionStatus type with draft→active lifecycle for AI-generated workflows, type all DB status fields with proper unions instead of plain strings
1063
- - Updated dependencies [387b2ee]
1064
- - Updated dependencies [32ed003]
1065
- - Updated dependencies [7d369f3]
1066
- - Updated dependencies [508a796]
1067
- - Updated dependencies [ffe83af]
1068
- - Updated dependencies [c7ff141]
1069
- - @pikku/core@0.12.3
package/run-tests.sh DELETED
@@ -1,68 +0,0 @@
1
- #!/bin/bash
2
-
3
- # Enable nullglob to handle cases where no files match the pattern
4
- shopt -s nullglob
5
-
6
- # Initialize variables for options
7
- watch_mode=false
8
- coverage_mode=false
9
-
10
- # Parse command-line options
11
- while [[ $# -gt 0 ]]; do
12
- case $1 in
13
- --watch)
14
- watch_mode=true
15
- shift
16
- ;;
17
- --coverage)
18
- coverage_mode=true
19
- shift
20
- ;;
21
- *)
22
- echo "Unknown option: $1"
23
- exit 1
24
- ;;
25
- esac
26
- done
27
-
28
- # Define the pattern to match your test files
29
- pattern="src/*.test.ts"
30
-
31
- # Expand the pattern into an array of files
32
- files=($(find src -type f -name "*.test.ts"))
33
-
34
- # Check if any files matched the pattern
35
- if [ ${#files[@]} -eq 0 ]; then
36
- echo "No test files found matching pattern: $pattern"
37
- if [ "${STRICT_TEST_DISCOVERY}" = "1" ]; then
38
- exit 1
39
- fi
40
- exit 0
41
- fi
42
-
43
- # Construct the bun command
44
- #
45
- # --parallel gives every test file its own worker process, which is what node's
46
- # `--test` did. Several suites keep module-level caches (a TypeScript program, a
47
- # temp fixture directory) that a shared process would leak between files.
48
- #
49
- # --timeout because bun caps a test at 5s by default where node had no cap, and
50
- # the suites that build real TypeScript programs run well past that.
51
- bun_cmd=(bun test --parallel --timeout 120000)
52
-
53
- # Append options based on flags
54
- if [ "$watch_mode" = true ]; then
55
- # --watch reruns in one process; parallel workers have nothing to watch.
56
- bun_cmd=(bun test --watch --timeout 120000)
57
- fi
58
-
59
- if [ "$coverage_mode" = true ]; then
60
- # --coverage-dir=. keeps lcov.info at the package root: CI merges every
61
- # package's report by prefixing its SF paths with the file's own directory,
62
- # so a nested coverage/ directory would re-root them one level too deep.
63
- bun_cmd+=(--coverage --coverage-reporter=lcov --coverage-reporter=text --coverage-dir=.)
64
- export PIKKU_TEST_COVERAGE=1
65
- fi
66
-
67
- # Execute the bun command with the expanded list of files
68
- "${bun_cmd[@]}" "${files[@]}"