@pikku/kysely 0.13.19 → 0.13.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/dist/src/index.d.ts +3 -2
  3. package/dist/src/index.js +2 -1
  4. package/dist/src/kysely-agent-run-state-service.js +2 -2
  5. package/dist/src/kysely-agent-storage-service.js +2 -2
  6. package/dist/src/kysely-audit-service.js +2 -2
  7. package/dist/src/kysely-channel-store.js +2 -2
  8. package/dist/src/kysely-credential-service.js +2 -2
  9. package/dist/src/kysely-deployment-service.js +2 -2
  10. package/dist/src/kysely-scope-service.js +2 -2
  11. package/dist/src/kysely-secret-service.js +2 -2
  12. package/dist/src/kysely-session-store.js +2 -2
  13. package/dist/src/kysely-tables.d.ts +74 -0
  14. package/dist/src/kysely-virtual-user-run-store.d.ts +7 -19
  15. package/dist/src/kysely-virtual-user-run-store.js +71 -4
  16. package/dist/src/kysely-virtual-user-schedule-store.d.ts +33 -0
  17. package/dist/src/kysely-virtual-user-schedule-store.js +151 -0
  18. package/dist/src/kysely-webhook-service.js +2 -2
  19. package/dist/src/kysely-workflow-mirror.d.ts +1 -1
  20. package/dist/src/kysely-workflow-mirror.js +3 -3
  21. package/dist/src/kysely-workflow-service.js +2 -2
  22. package/dist/src/schema/agent.schema.js +1 -0
  23. package/dist/src/schema/audit.schema.d.ts +5 -4
  24. package/dist/src/schema/audit.schema.js +6 -4
  25. package/dist/src/schema/channel.schema.js +1 -0
  26. package/dist/src/schema/credential.schema.js +1 -0
  27. package/dist/src/schema/index.d.ts +48 -26
  28. package/dist/src/schema/index.js +59 -46
  29. package/dist/src/schema/pikku-schema.types.d.ts +20 -0
  30. package/dist/src/schema/scope.schema.js +1 -0
  31. package/dist/src/schema/virtual-user-schedule.schema.d.ts +15 -0
  32. package/dist/src/schema/virtual-user-schedule.schema.js +43 -0
  33. package/dist/src/schema/virtual-user.schema.d.ts +2 -1
  34. package/dist/src/schema/virtual-user.schema.js +41 -2
  35. package/dist/src/schema/webhook.schema.js +1 -0
  36. package/dist/src/schema/workflow.schema.js +1 -0
  37. package/dist/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +2 -2
  39. package/src/index.ts +15 -2
  40. package/src/kysely-agent-run-state-service.ts +2 -2
  41. package/src/kysely-agent-storage-service.ts +2 -2
  42. package/src/kysely-audit-service.test.ts +2 -10
  43. package/src/kysely-audit-service.ts +2 -2
  44. package/src/kysely-channel-store.ts +2 -2
  45. package/src/kysely-credential-service.ts +2 -2
  46. package/src/kysely-deployment-service.ts +2 -2
  47. package/src/kysely-scope-service.test.ts +2 -0
  48. package/src/kysely-scope-service.ts +2 -2
  49. package/src/kysely-secret-service.ts +2 -2
  50. package/src/kysely-services.test.ts +26 -0
  51. package/src/kysely-session-store.ts +2 -2
  52. package/src/kysely-tables.test.ts +1 -12
  53. package/src/kysely-tables.ts +77 -0
  54. package/src/kysely-virtual-user-run-store.test.ts +164 -7
  55. package/src/kysely-virtual-user-run-store.ts +92 -6
  56. package/src/kysely-virtual-user-schedule-store.test.ts +187 -0
  57. package/src/kysely-virtual-user-schedule-store.ts +187 -0
  58. package/src/kysely-webhook-service.test.ts +4 -0
  59. package/src/kysely-webhook-service.ts +2 -2
  60. package/src/kysely-workflow-mirror.test.ts +2 -0
  61. package/src/kysely-workflow-mirror.ts +3 -3
  62. package/src/kysely-workflow-service.test.ts +3 -1
  63. package/src/kysely-workflow-service.ts +2 -2
  64. package/src/schema/agent.schema.ts +1 -0
  65. package/src/schema/audit.schema.ts +6 -4
  66. package/src/schema/channel.schema.ts +1 -0
  67. package/src/schema/credential.schema.ts +1 -0
  68. package/src/schema/index.ts +70 -52
  69. package/src/schema/pikku-schema.types.ts +20 -0
  70. package/src/schema/schema.test.ts +86 -29
  71. package/src/schema/scope.schema.ts +1 -0
  72. package/src/schema/virtual-user-schedule.schema.ts +48 -0
  73. package/src/schema/virtual-user.schema.ts +43 -2
  74. package/src/schema/webhook.schema.ts +1 -0
  75. package/src/schema/workflow.schema.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,177 @@
1
+ ## 0.13.21
2
+
3
+ ### Patch Changes
4
+
5
+ - cfd364a: Remove the last `@pikku/core/ecosystem` references and guard against new ones
6
+
7
+ `@pikku/kysely`'s workflow-service test still imported `StepState` from
8
+ `@pikku/core/ecosystem/workflow`, a subpath that no longer exists in
9
+ `@pikku/core`'s `exports`. Nothing caught it: the import is type-only, so tsx
10
+ erases it before it can fail at runtime, and the package tsconfig excludes
11
+ `**/*.test.ts`, so `yarn tsc` never saw it either. It now imports from
12
+ `@pikku/core/workflow`.
13
+
14
+ A new guard test in `@pikku/core` scans the repository for the dead specifier
15
+ and fails if one comes back, so the next stale import is a red test rather than
16
+ a silent `any`.
17
+
18
+ - 05e47cf: feat(virtual-user): keep the transcript a run already produced
19
+
20
+ The engine returns `intents` and `steps` on every run — what the user set out
21
+ to do, and every turn it took getting there — and `VirtualUserRunOutcome` kept
22
+ neither. The record held counts and findings, so the one question anybody
23
+ actually asks of a completed run ("what did it _do_?") had no answer anywhere,
24
+ even though the answer had been computed and thrown away a moment earlier.
25
+
26
+ `VirtualUserRunOutcome` now carries both, and `VirtualUserRunStore` gains a
27
+ `steps(runId, options?)` read. Intents ride on the run record: there are a
28
+ handful of them and every read of the run wants them. Steps get their own
29
+ `virtualUserRunStep` table, because a run at a 500-step budget carries more
30
+ transcript than every other column together and `list()` would pay for it on
31
+ every row.
32
+
33
+ Three things the kysely store had to get right, all of them driver differences
34
+ rather than design:
35
+
36
+ - steps are inserted in chunks of 50, because a bare sqlite driver binds at
37
+ most 999 variables per statement and ten columns times a 500-step budget is
38
+ five thousand — an un-chunked insert fails on long runs, which are the
39
+ interesting ones;
40
+ - `ok` is stored as 0 or 1, since a bare driver cannot bind a boolean at all
41
+ and `SerializePlugin` is not installed everywhere;
42
+ - `response` is stored JSON-encoded, because a truncated API response usually
43
+ starts with a brace and `SerializePlugin` would otherwise read it back as an
44
+ object rather than the string the engine saw.
45
+
46
+ Completing a run that does not exist no longer writes steps: there is no
47
+ foreign key to refuse them and nothing would ever read or reap them.
48
+
49
+ **This adds a table to the `virtualUser` schema**, and the runtime creates
50
+ nothing: a database that already has `virtualUserRun` gets the store's own
51
+ refusal at startup until `pikku db generate` writes the migration and
52
+ `pikku db migrate` applies it. Landing it now costs nothing, because
53
+ `scaffold.virtualUser` is not yet switched on anywhere.
54
+
55
+ - 05e47cf: feat(virtual-user): put each persona on its own clock
56
+
57
+ A budget says where one run stops. Nothing said how often a persona should use
58
+ the application, so in practice each one ran whenever somebody remembered — and
59
+ what actually tells you about a product is the same user coming back over a
60
+ fortnight.
61
+
62
+ Each persona now gets a row rather than a bigger budget. `virtualUserSchedule`
63
+ holds `enabled`, the disposition and goals to run with, an interval **range**,
64
+ and `nextRunAt`. `tickVirtualUserSchedules` acts on whichever rows are due:
65
+
66
+ ```ts
67
+ wireScheduler({
68
+ name: 'virtualUsers',
69
+ schedule: '0 * * * *',
70
+ func: tickVirtualUserSchedules,
71
+ })
72
+ ```
73
+
74
+ The tick is generated and wired by nobody, deliberately. A scaffolded
75
+ `wireScheduler` would start spending an application's model budget the moment
76
+ somebody ran `pikku all`, on a host that may not run schedulers at all. Tick
77
+ resolution bounds how late a due persona is, never how often it runs.
78
+
79
+ Three things it does that are easy to leave out:
80
+
81
+ - The next due time is written **before** the run is dispatched, so a tick that
82
+ dies halfway cannot hand the same persona to the next one. A dispatch that
83
+ throws waits a full interval instead of retrying every minute for a week.
84
+ That write is a compare-and-set against the `nextRunAt` the tick read, so it
85
+ is also how a tick _wins_ the persona: two processes on the same cron see the
86
+ same due row, and only the one whose claim lands dispatches.
87
+ - A persona whose previous run is still `running` is skipped, not queued. Two
88
+ copies of the same user acting at once is a different test, and its findings
89
+ do not reproduce.
90
+ - A run still `running` after two hours is failed. Without that, one restart
91
+ mid-run blocks that persona's schedule permanently — which is where the
92
+ stranded-record cost of not using a queue finally gets paid.
93
+
94
+ Reschedule-on-completion was the other candidate and is worse in exactly one
95
+ way, fatally: a crash between finishing and scheduling ends the persona forever,
96
+ and the evidence is an absence.
97
+
98
+ New: `VirtualUserScheduleStore` in core (with the tick, `isDue` and `nextRunAt`
99
+ as pure functions), `KyselyVirtualUserScheduleStore` and its own schema —
100
+ its own rather than a third table in `virtualUserSchema`, and owned by its own
101
+ store, so a project that records runs and never wants them unattended carries no
102
+ cadence table. `scaffold.virtualUser` gains `setVirtualUserSchedule`,
103
+ `listVirtualUserSchedules` and the tick, behind a new `virtualUser:schedule`
104
+ scope: starting a run spends money once with a caller watching, while writing a
105
+ schedule spends it repeatedly with nobody there.
106
+
107
+ The console's Virtual Users screen gains a **Run now** button beside a persona's
108
+ run history, gated on `pikku:console:virtualUsers:run`. It dispatches the
109
+ project's own `runVirtualUser` rather than starting a run itself, so a run the
110
+ application would refuse — an acted-upon persona, a non-accountable disposition
111
+ in production — is still refused.
112
+
113
+ - Updated dependencies [3c0012c]
114
+ - Updated dependencies [05e47cf]
115
+ - Updated dependencies [cfd364a]
116
+ - Updated dependencies [05e47cf]
117
+ - Updated dependencies [05e47cf]
118
+ - Updated dependencies [05e47cf]
119
+ - @pikku/core@0.12.90
120
+
121
+ ## 0.13.20
122
+
123
+ ### Patch Changes
124
+
125
+ - 274cab3: The singleton intersection moves to the leaves that name it, the runtime stops
126
+ creating schema, and `db generate` writes only the runtime tables a project's
127
+ services own
128
+
129
+ `WiredSingletonServices` was exported from the generated function leaf so the
130
+ auth leaf could import it. Nothing outside a generated leaf ever names it —
131
+ emit declarations for a project of any size and it appears in none of them —
132
+ so the auth and middleware leaves derive the intersection themselves and the
133
+ function leaf keeps it private. `WiredServices` stays exported: 147 `.d.ts`
134
+ files name it, and unexporting it asks every wired module to name each member
135
+ service through a specifier it does not have.
136
+
137
+ `ensurePikkuSchema` is gone. `requirePikkuSchema` replaces it: a service calls
138
+ it at boot, it looks, and it issues no DDL at all. `pikku db generate` writes
139
+ the declaration down as a migration and `pikku db migrate` applies it, and
140
+ those two are now the only way pikku's runtime tables come into existence. A
141
+ service that finds them missing says so and stops, naming both commands.
142
+ Half-present is no longer a distinct case — the remedy is the same migration
143
+ either way. `audit` and `virtual-user` join `pikkuSchemas` as a consequence:
144
+ boot was the only thing that had ever created them.
145
+
146
+ `pikku db generate` applied all of `pikkuSchemas`, so a project with no agents,
147
+ no channels and no workflows still had `agent_threads`, `channels` and
148
+ `workflow_runs` written into its migrations, and then carried them forever. A
149
+ schema now names the services that own it, and generation gates on
150
+ `requiredServices` — the set the inspector already builds for service
151
+ tree-shaking. The gate is one-sided: a schema that names no owner is always
152
+ written, because the session and secret stores and the deployment record are
153
+ reached by the runtime itself and nothing in a project's source implies them.
154
+ Declared scopes now imply `scopeService`, which nothing destructures because
155
+ the generated auth layer is what reaches it.
156
+
157
+ Drift keeps asking the unscoped question. A table already in a database has to
158
+ stay recognisable as a runtime table after the service that needed it is
159
+ dropped — scoping it there would report those tables as unexplained.
160
+
161
+ Every project in this repo that had been relying on boot-time creation now says
162
+ where its tables come from. `createConfig` moves into its own `config.ts` in the
163
+ templates — `pikku db` looks for it there — and the three postgres templates plus
164
+ the workflow verifier declare `postgresUrl` and run `pikku db generate && pikku
165
+ db migrate` before the server starts, from the single connection string their
166
+ runtime opens. The e2e harness cannot: its databases are in-memory sqlite built
167
+ inside the services factory, so nothing outside the process can migrate them. It
168
+ applies the schemas it owns with `applyPikkuSchemas` instead — the same DDL, run
169
+ by the one process that has the database.
170
+
171
+ - Updated dependencies [32616af]
172
+ - Updated dependencies [6848cd9]
173
+ - @pikku/core@0.12.89
174
+
1
175
  ## 0.13.19
2
176
 
3
177
  ### Patch Changes
@@ -15,10 +15,11 @@ export { KyselyWebhookService } from './kysely-webhook-service.js';
15
15
  export { createAuditedKysely, type CreateAuditedKyselyOptions, } from './create-audited-kysely.js';
16
16
  export { KyselyAuditService } from './kysely-audit-service.js';
17
17
  export { KyselyVirtualUserRunStore } from './kysely-virtual-user-run-store.js';
18
+ export { KyselyVirtualUserScheduleStore } from './kysely-virtual-user-schedule-store.js';
18
19
  export { SerializePlugin, BaseSerializePlugin, type Serializer, type Deserializer, } from './serialize-plugin.js';
19
20
  export { createCoercionPlugin, type ColumnKind, type CoercionMap, type CreateCoercionPluginOptions, } from './coercion-plugin.js';
20
- export { auditSchema, pikkuSchemas, applyPikkuSchemas, compilePikkuSchemas, ensurePikkuSchema, type EnsureOutcome, resolveRequirements, type PikkuSchema, type RequiredTypes, type UnmetRequirement, type SchemaStatementFactory, } from './schema/index.js';
21
+ export { agentSchema, auditSchema, channelSchema, credentialSchema, deploymentSchema, scopeSchema, secretSchema, sessionSchema, virtualUserSchema, webhookSchema, workflowSchema, pikkuSchemas, requiredPikkuSchemas, applyPikkuSchemas, compilePikkuSchemas, requirePikkuSchema, resolveRequirements, type PikkuSchema, type RequiredTypes, type UnmetRequirement, type SchemaStatementFactory, } from './schema/index.js';
21
22
  export type { KyselyPikkuDB } from './kysely-tables.js';
22
23
  export type { WorkflowRunService } from '@pikku/core/workflow';
23
24
  export type { AgentRunService, AgentRunRow } from '@pikku/core/agent';
24
- export type { VirtualUserRunStore, VirtualUserRunRecord, } from '@pikku/core/virtual-user';
25
+ export type { VirtualUserRunStore, VirtualUserRunRecord, VirtualUserScheduleStore, VirtualUserScheduleRecord, } from '@pikku/core/virtual-user';
package/dist/src/index.js CHANGED
@@ -15,6 +15,7 @@ export { KyselyWebhookService } from './kysely-webhook-service.js';
15
15
  export { createAuditedKysely, } from './create-audited-kysely.js';
16
16
  export { KyselyAuditService } from './kysely-audit-service.js';
17
17
  export { KyselyVirtualUserRunStore } from './kysely-virtual-user-run-store.js';
18
+ export { KyselyVirtualUserScheduleStore } from './kysely-virtual-user-schedule-store.js';
18
19
  export { SerializePlugin, BaseSerializePlugin, } from './serialize-plugin.js';
19
20
  export { createCoercionPlugin, } from './coercion-plugin.js';
20
- export { auditSchema, pikkuSchemas, applyPikkuSchemas, compilePikkuSchemas, ensurePikkuSchema, resolveRequirements, } from './schema/index.js';
21
+ export { agentSchema, auditSchema, channelSchema, credentialSchema, deploymentSchema, scopeSchema, secretSchema, sessionSchema, virtualUserSchema, webhookSchema, workflowSchema, pikkuSchemas, requiredPikkuSchemas, applyPikkuSchemas, compilePikkuSchemas, requirePikkuSchema, resolveRequirements, } from './schema/index.js';
@@ -1,5 +1,5 @@
1
1
  import { sql } from 'kysely';
2
- import { ensurePikkuSchema } from './schema/index.js';
2
+ import { requirePikkuSchema } from './schema/index.js';
3
3
  import { agentSchema } from './schema/agent.schema.js';
4
4
  import { getRunScores, saveRunScore } from './kysely-agent-run-scores.js';
5
5
  export class KyselyAgentRunStateService {
@@ -11,7 +11,7 @@ export class KyselyAgentRunStateService {
11
11
  async init() {
12
12
  if (this.initialized)
13
13
  return;
14
- await ensurePikkuSchema(this.db, agentSchema);
14
+ await requirePikkuSchema(this.db, agentSchema);
15
15
  this.initialized = true;
16
16
  }
17
17
  async createRun(run) {
@@ -1,6 +1,6 @@
1
1
  import { parseJson } from './kysely-json.js';
2
2
  import { getRunScores, saveRunScore } from './kysely-agent-run-scores.js';
3
- import { ensurePikkuSchema } from './schema/index.js';
3
+ import { requirePikkuSchema } from './schema/index.js';
4
4
  import { agentSchema } from './schema/agent.schema.js';
5
5
  export class KyselyAgentStorageService {
6
6
  db;
@@ -11,7 +11,7 @@ export class KyselyAgentStorageService {
11
11
  async init() {
12
12
  if (this.initialized)
13
13
  return;
14
- await ensurePikkuSchema(this.db, agentSchema);
14
+ await requirePikkuSchema(this.db, agentSchema);
15
15
  this.initialized = true;
16
16
  }
17
17
  async createThread(resourceId, options) {
@@ -1,4 +1,4 @@
1
- import { ensurePikkuSchema } from './schema/index.js';
1
+ import { requirePikkuSchema } from './schema/index.js';
2
2
  import { auditSchema } from './schema/audit.schema.js';
3
3
  const jsonOrNull = (v) => v != null ? JSON.stringify(v) : null;
4
4
  const parseJson = (v) => {
@@ -90,7 +90,7 @@ export class KyselyAuditService {
90
90
  async init() {
91
91
  if (this.initialized)
92
92
  return;
93
- await ensurePikkuSchema(this.db, auditSchema);
93
+ await requirePikkuSchema(this.db, auditSchema);
94
94
  this.initialized = true;
95
95
  }
96
96
  async audit(event) {
@@ -1,6 +1,6 @@
1
1
  import { ChannelStore } from '@pikku/core/channel';
2
2
  import { parseJson } from './kysely-json.js';
3
- import { ensurePikkuSchema } from './schema/index.js';
3
+ import { requirePikkuSchema } from './schema/index.js';
4
4
  import { channelSchema } from './schema/channel.schema.js';
5
5
  export class KyselyChannelStore extends ChannelStore {
6
6
  db;
@@ -12,7 +12,7 @@ export class KyselyChannelStore extends ChannelStore {
12
12
  async init() {
13
13
  if (this.initialized)
14
14
  return;
15
- await ensurePikkuSchema(this.db, channelSchema);
15
+ await requirePikkuSchema(this.db, channelSchema);
16
16
  this.initialized = true;
17
17
  }
18
18
  async addChannel({ channelId, channelName, openingData, }) {
@@ -1,5 +1,5 @@
1
1
  import { deriveKEK, envelopeDecrypt, envelopeEncrypt, envelopeRewrap, generateKEKSalt, } from '@pikku/core/crypto-utils';
2
- import { ensurePikkuSchema } from './schema/index.js';
2
+ import { requirePikkuSchema } from './schema/index.js';
3
3
  import { credentialSchema } from './schema/credential.schema.js';
4
4
  export class KyselyCredentialService {
5
5
  db;
@@ -22,7 +22,7 @@ export class KyselyCredentialService {
22
22
  async init() {
23
23
  if (this.initialized)
24
24
  return;
25
- await ensurePikkuSchema(this.db, credentialSchema);
25
+ await requirePikkuSchema(this.db, credentialSchema);
26
26
  this.initialized = true;
27
27
  }
28
28
  async logAudit(name, userId, action) {
@@ -1,6 +1,6 @@
1
1
  import { buildRemoteHeaders } from '@pikku/core/remote';
2
2
  import { getAllFunctionNames } from '@pikku/core/function';
3
- import { ensurePikkuSchema } from './schema/index.js';
3
+ import { requirePikkuSchema } from './schema/index.js';
4
4
  import { deploymentSchema } from './schema/deployment.schema.js';
5
5
  export class KyselyDeploymentService {
6
6
  db;
@@ -21,7 +21,7 @@ export class KyselyDeploymentService {
21
21
  async init() {
22
22
  if (this.initialized)
23
23
  return;
24
- await ensurePikkuSchema(this.db, deploymentSchema);
24
+ await requirePikkuSchema(this.db, deploymentSchema);
25
25
  this.initialized = true;
26
26
  }
27
27
  async start(config) {
@@ -1,5 +1,5 @@
1
1
  import { SystemRoleImmutableError, SystemRoleShadowedError, } from '@pikku/core/errors';
2
- import { ensurePikkuSchema } from './schema/index.js';
2
+ import { requirePikkuSchema } from './schema/index.js';
3
3
  import { scopeSchema } from './schema/scope.schema.js';
4
4
  /**
5
5
  * Resolves and administers user scopes against four self-created tables.
@@ -22,7 +22,7 @@ export class KyselyScopeService {
22
22
  async init() {
23
23
  if (this.initialized)
24
24
  return;
25
- await ensurePikkuSchema(this.db, scopeSchema);
25
+ await requirePikkuSchema(this.db, scopeSchema);
26
26
  this.initialized = true;
27
27
  }
28
28
  /**
@@ -1,6 +1,6 @@
1
1
  import { deriveKEK, envelopeDecrypt, envelopeEncrypt, envelopeRewrap, generateKEKSalt, } from '@pikku/core/crypto-utils';
2
2
  import { createSecretValue, isSecretValue } from '@pikku/core/classification';
3
- import { ensurePikkuSchema } from './schema/index.js';
3
+ import { requirePikkuSchema } from './schema/index.js';
4
4
  import { secretSchema } from './schema/secret.schema.js';
5
5
  export class KyselySecretService {
6
6
  db;
@@ -23,7 +23,7 @@ export class KyselySecretService {
23
23
  async init() {
24
24
  if (this.initialized)
25
25
  return;
26
- await ensurePikkuSchema(this.db, secretSchema);
26
+ await requirePikkuSchema(this.db, secretSchema);
27
27
  this.initialized = true;
28
28
  }
29
29
  async logAudit(secretKey, action) {
@@ -1,5 +1,5 @@
1
1
  import { parseJson } from './kysely-json.js';
2
- import { ensurePikkuSchema } from './schema/index.js';
2
+ import { requirePikkuSchema } from './schema/index.js';
3
3
  import { sessionSchema } from './schema/session.schema.js';
4
4
  export class KyselySessionStore {
5
5
  db;
@@ -10,7 +10,7 @@ export class KyselySessionStore {
10
10
  async init() {
11
11
  if (this.initialized)
12
12
  return;
13
- await ensurePikkuSchema(this.db, sessionSchema);
13
+ await requirePikkuSchema(this.db, sessionSchema);
14
14
  this.initialized = true;
15
15
  }
16
16
  async get(pikkuUserId) {
@@ -277,6 +277,7 @@ export interface VirtualUserRunTable {
277
277
  goals: Generated<string>;
278
278
  memory: Generated<string>;
279
279
  findings: Generated<string>;
280
+ intents: Generated<string>;
280
281
  tally: string | null;
281
282
  stoppedBy: string | null;
282
283
  error: string | null;
@@ -289,6 +290,76 @@ export interface VirtualUserRunTable {
289
290
  createdAt: Generated<Date | string>;
290
291
  finishedAt: Date | string | null;
291
292
  }
293
+ /**
294
+ * One audit event. Every column is text on every engine so a locally-run
295
+ * project and a deployed stage write rows the same reader can read; the JSON
296
+ * columns (`tables`, `changedCols`, `event`, `old`, `data`) are serialised by
297
+ * {@link KyselyAuditService} rather than by a plugin.
298
+ */
299
+ export interface AuditTable {
300
+ auditId: string;
301
+ /** ISO 8601, because string ordering is chronological ordering. */
302
+ occurredAt: string;
303
+ type: string;
304
+ source: Generated<string>;
305
+ outcome: string | null;
306
+ functionId: string | null;
307
+ wireType: string | null;
308
+ traceId: string | null;
309
+ transactionId: string | null;
310
+ queryId: string | null;
311
+ userId: string | null;
312
+ orgId: string | null;
313
+ pikkuUserId: string | null;
314
+ tables: string | null;
315
+ changedCols: string | null;
316
+ event: string | null;
317
+ old: string | null;
318
+ data: string | null;
319
+ }
320
+ /**
321
+ * One turn of a run's transcript.
322
+ *
323
+ * Its own table rather than a column on {@link VirtualUserRunTable}: a run at a
324
+ * 500-step budget carries more transcript than every other column together, and
325
+ * the run list would pay for it on every row.
326
+ */
327
+ export interface VirtualUserRunStepTable {
328
+ runId: string;
329
+ /** The engine's step number. `stepIndex` because `index` is reserved in mysql. */
330
+ stepIndex: number;
331
+ intentId: string | null;
332
+ /** JSON: the action the engine scheduled, or the `invalid` shape. */
333
+ action: string;
334
+ status: number | null;
335
+ /** 0 or 1: not every driver in this package can bind a boolean. */
336
+ ok: number | null;
337
+ /** JSON-encoded, so a brace-leading response is not read back as an object. */
338
+ response: string | null;
339
+ /** JSON array, null when the turn produced no finding. */
340
+ findingKinds: string | null;
341
+ tokensIn: Generated<number>;
342
+ tokensOut: Generated<number>;
343
+ }
344
+ /**
345
+ * One persona's cadence. Nullable timestamps and 0/1 flags for the same driver
346
+ * reasons as {@link VirtualUserRunTable}.
347
+ */
348
+ export interface VirtualUserScheduleTable {
349
+ persona: string;
350
+ /** 0 or 1: not every driver in this package can bind a boolean. */
351
+ enabled: Generated<number>;
352
+ disposition: string;
353
+ goals: Generated<string>;
354
+ /** JSON, or null for the engine's own default budget. */
355
+ budget: string | null;
356
+ /** BIGINT, so a driver may hand either back. */
357
+ minIntervalMs: string | number | bigint;
358
+ maxIntervalMs: string | number | bigint;
359
+ nextRunAt: Date | string;
360
+ lastRunId: string | null;
361
+ lastRunAt: Date | string | null;
362
+ }
292
363
  export interface KyselyPikkuDB {
293
364
  pikkuScopes: PikkuScopesTable;
294
365
  pikkuRoles: PikkuRolesTable;
@@ -319,4 +390,7 @@ export interface KyselyPikkuDB {
319
390
  webhookDelivery: WebhookDeliveryTable;
320
391
  webhookDeliveryAttempt: WebhookDeliveryAttemptTable;
321
392
  virtualUserRun: VirtualUserRunTable;
393
+ virtualUserRunStep: VirtualUserRunStepTable;
394
+ virtualUserSchedule: VirtualUserScheduleTable;
395
+ audit: AuditTable;
322
396
  }
@@ -1,25 +1,7 @@
1
1
  import type { Kysely } from 'kysely';
2
+ import type { StepRecord } from '@pikku/core/virtual-user';
2
3
  import type { VirtualUserRunOutcome, VirtualUserRunRecord, VirtualUserRunStart, VirtualUserRunStore } from '@pikku/core/virtual-user';
3
4
  import type { KyselyPikkuDB } from './kysely-tables.js';
4
- /**
5
- * Records virtual-user runs in a `virtualUserRun` table.
6
- *
7
- * The store is the whole record of a run: `runVirtualUser` returns as soon as
8
- * the row exists and the engine keeps going without it, so nothing else knows
9
- * the run happened. That is why `fail()` exists as its own call — a run that
10
- * crashed and a run that found nothing are different answers, and a row left at
11
- * `running` is neither.
12
- *
13
- * Works with or without `SerializePlugin`, which not every project installs: the
14
- * JSON columns are written as strings (which the plugin passes through) and read
15
- * back through `parseJson` (which passes through what the plugin already
16
- * parsed). Timestamps are written as ISO strings for the same reason — a bare
17
- * SQLite driver cannot bind a `Date` at all.
18
- *
19
- * SECURITY: an `adversarial` run's findings are working exploits carrying live
20
- * ids. This is a privileged store; the scaffolded reads are scope-gated, and
21
- * exposing these rows more widely publishes your own exploits.
22
- */
23
5
  export declare class KyselyVirtualUserRunStore implements VirtualUserRunStore {
24
6
  private db;
25
7
  private initialized;
@@ -39,5 +21,11 @@ export declare class KyselyVirtualUserRunStore implements VirtualUserRunStore {
39
21
  limit?: number;
40
22
  offset?: number;
41
23
  }): Promise<VirtualUserRunRecord[]>;
24
+ steps(runId: string, options?: {
25
+ limit?: number;
26
+ offset?: number;
27
+ }): Promise<StepRecord[]>;
28
+ private writeSteps;
29
+ private toStep;
42
30
  private toRecord;
43
31
  }
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { parseJson } from './kysely-json.js';
3
- import { ensurePikkuSchema } from './schema/index.js';
3
+ import { requirePikkuSchema } from './schema/index.js';
4
4
  import { virtualUserSchema } from './schema/virtual-user.schema.js';
5
5
  /**
6
6
  * Records virtual-user runs in a `virtualUserRun` table.
@@ -21,6 +21,15 @@ import { virtualUserSchema } from './schema/virtual-user.schema.js';
21
21
  * ids. This is a privileged store; the scaffolded reads are scope-gated, and
22
22
  * exposing these rows more widely publishes your own exploits.
23
23
  */
24
+ /**
25
+ * How many step rows go in one insert.
26
+ *
27
+ * Not a performance dial. A bare SQLite driver binds at most 999 variables per
28
+ * statement, and ten columns times a 500-step budget is five thousand — so an
29
+ * un-chunked insert does not fail on a long run, it fails on the *interesting*
30
+ * long run, which is the one nobody wants to lose.
31
+ */
32
+ const STEP_INSERT_CHUNK = 50;
24
33
  export class KyselyVirtualUserRunStore {
25
34
  db;
26
35
  initialized = false;
@@ -35,7 +44,7 @@ export class KyselyVirtualUserRunStore {
35
44
  async init() {
36
45
  if (this.initialized)
37
46
  return;
38
- await ensurePikkuSchema(this.db, virtualUserSchema);
47
+ await requirePikkuSchema(this.db, virtualUserSchema);
39
48
  this.initialized = true;
40
49
  }
41
50
  async start(run) {
@@ -61,7 +70,7 @@ export class KyselyVirtualUserRunStore {
61
70
  }
62
71
  async complete(runId, outcome) {
63
72
  await this.init();
64
- await this.db
73
+ const updated = await this.db
65
74
  .updateTable('virtualUserRun')
66
75
  .set({
67
76
  status: 'completed',
@@ -70,11 +79,17 @@ export class KyselyVirtualUserRunStore {
70
79
  // Overwritten rather than merged: the engine's memory already carries
71
80
  // what it was given, plus what it learned on the way.
72
81
  memory: JSON.stringify(outcome.memory),
82
+ intents: JSON.stringify(outcome.intents),
73
83
  stoppedBy: outcome.stoppedBy,
74
84
  finishedAt: new Date().toISOString(),
75
85
  })
76
86
  .where('runId', '=', runId)
77
- .execute();
87
+ .executeTakeFirst();
88
+ // Nothing owns steps written against a run that is not there, and nothing
89
+ // would ever read or reap them — there is no foreign key to refuse them.
90
+ if (updated.numUpdatedRows === 0n)
91
+ return;
92
+ await this.writeSteps(runId, outcome.steps);
78
93
  }
79
94
  async fail(runId, error) {
80
95
  await this.init();
@@ -110,6 +125,57 @@ export class KyselyVirtualUserRunStore {
110
125
  .execute();
111
126
  return rows.map((row) => this.toRecord(row));
112
127
  }
128
+ async steps(runId, options) {
129
+ await this.init();
130
+ const rows = await this.db
131
+ .selectFrom('virtualUserRunStep')
132
+ .selectAll()
133
+ .where('runId', '=', runId)
134
+ .orderBy('stepIndex', 'asc')
135
+ .limit(options?.limit ?? 1000)
136
+ .offset(options?.offset ?? 0)
137
+ .execute();
138
+ return rows.map((row) => this.toStep(row));
139
+ }
140
+ async writeSteps(runId, steps) {
141
+ for (let at = 0; at < steps.length; at += STEP_INSERT_CHUNK) {
142
+ const chunk = steps.slice(at, at + STEP_INSERT_CHUNK);
143
+ await this.db
144
+ .insertInto('virtualUserRunStep')
145
+ .values(chunk.map((step) => ({
146
+ runId,
147
+ stepIndex: step.index,
148
+ intentId: step.intentId ?? null,
149
+ action: JSON.stringify(step.action),
150
+ status: step.status ?? null,
151
+ ok: step.ok === undefined ? null : step.ok ? 1 : 0,
152
+ response: step.response === undefined
153
+ ? null
154
+ : JSON.stringify(step.response),
155
+ findingKinds: step.findingKinds
156
+ ? JSON.stringify(step.findingKinds)
157
+ : null,
158
+ tokensIn: step.tokensIn,
159
+ tokensOut: step.tokensOut,
160
+ })))
161
+ .execute();
162
+ }
163
+ }
164
+ toStep(row) {
165
+ return {
166
+ index: Number(row.stepIndex),
167
+ ...(row.intentId ? { intentId: row.intentId } : {}),
168
+ action: parseJson(row.action),
169
+ ...(row.status === null ? {} : { status: Number(row.status) }),
170
+ ...(row.ok === null ? {} : { ok: Boolean(row.ok) }),
171
+ ...(row.response === null ? {} : { response: parseJson(row.response) }),
172
+ ...(row.findingKinds
173
+ ? { findingKinds: parseJson(row.findingKinds) }
174
+ : {}),
175
+ tokensIn: Number(row.tokensIn),
176
+ tokensOut: Number(row.tokensOut),
177
+ };
178
+ }
113
179
  toRecord(row) {
114
180
  return {
115
181
  runId: row.runId,
@@ -120,6 +186,7 @@ export class KyselyVirtualUserRunStore {
120
186
  goals: parseJson(row.goals) ?? [],
121
187
  memory: parseJson(row.memory) ?? {},
122
188
  findings: parseJson(row.findings) ?? [],
189
+ intents: parseJson(row.intents) ?? [],
123
190
  tally: row.tally ? (parseJson(row.tally) ?? null) : null,
124
191
  stoppedBy: row.stoppedBy,
125
192
  error: row.error,
@@ -0,0 +1,33 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { VirtualUserScheduleInput, VirtualUserScheduleRecord, VirtualUserScheduleStore } from '@pikku/core/virtual-user';
3
+ import type { KyselyPikkuDB } from './kysely-tables.js';
4
+ /**
5
+ * Keeps each persona's cadence in a `virtualUserSchedule` table.
6
+ *
7
+ * Wiring this is what turns virtual users from something a person triggers into
8
+ * something that happens: without it, {@link KyselyVirtualUserRunStore} still
9
+ * records every run, and every run still had someone behind it. Two services
10
+ * rather than one so that an app can want the history without wanting the bill.
11
+ *
12
+ * Reads and writes the same way the run store does — JSON as text, timestamps
13
+ * as ISO strings, flags as 0/1 — so a project on SQLite and one on Postgres
14
+ * hold the same bytes and `SerializePlugin` is optional on both.
15
+ */
16
+ export declare class KyselyVirtualUserScheduleStore implements VirtualUserScheduleStore {
17
+ private db;
18
+ private initialized;
19
+ constructor(db: Kysely<KyselyPikkuDB>);
20
+ init(): Promise<void>;
21
+ set(schedule: VirtualUserScheduleInput): Promise<VirtualUserScheduleRecord>;
22
+ get(persona: string): Promise<VirtualUserScheduleRecord | null>;
23
+ list(): Promise<VirtualUserScheduleRecord[]>;
24
+ due(now: Date): Promise<VirtualUserScheduleRecord[]>;
25
+ claim(persona: string, claim: {
26
+ from: Date;
27
+ nextRunAt: Date;
28
+ runId: string | null;
29
+ at: Date;
30
+ }): Promise<boolean>;
31
+ remove(persona: string): Promise<void>;
32
+ private toRecord;
33
+ }