@fickydev/pigent 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,17 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.1.18 - 2026-05-18
4
+
5
+ ### Added
6
+
7
+ - Added per-agent skill loading. Agent `skills[]` (relative to agent dir) and profile `defaultSkills[]` (relative to profile dir) are now resolved and passed to Pi SDK via `DefaultResourceLoader.additionalSkillPaths`. Skills are discovered and injected into the system prompt by the Pi SDK.
8
+ - Added `LoadedProfileConfig` type tracking profile `baseDir` for path resolution.
9
+
10
+ ## 0.1.17 - 2026-05-18
11
+
12
+ ### Added
13
+
14
+ - Added per-task `maxRunsPerHour` rate limit (default 12) to the scheduler. Skips execution when the cap is reached within the rolling hour window. Configurable per task in `pigent.yaml` task configs. DB-created tasks use the default unless the column is set.
4
15
 
5
16
  ## 0.1.16 - 2026-05-18
6
17
 
package/TODO.md CHANGED
@@ -144,8 +144,8 @@
144
144
  - [x] Reuse Pi session files across messages
145
145
  - [x] Self-heal missing runtime `agent_sessions` columns after partial/mismatched migration state
146
146
  - [x] Make README more end-user facing
147
- - [ ] Confirm per-agent system prompt injection
148
- - [ ] Confirm per-agent skills loading
147
+ - [x] Confirm per-agent system prompt injection
148
+ - [x] Confirm per-agent skills loading
149
149
  - [ ] Confirm per-agent extensions loading
150
150
  - [x] Prototype one prompt through Pi SDK
151
151
  - [x] Decide CLI fallback strategy if SDK gaps appear
@@ -166,7 +166,7 @@
166
166
  - [x] Add `/task remove <id>` command
167
167
  - [x] Scheduler loads tasks from both config file + DB
168
168
  - [x] Share session lock between Scheduler and AgentRunner (prevent concurrent Pi runs for same agent+chat)
169
- - [ ] Add max task runs per hour rate limit
169
+ - [x] Add max task runs per hour rate limit
170
170
  - [ ] Add `/task status` or similar command
171
171
 
172
172
  ## Policy And Safety
@@ -0,0 +1 @@
1
+ ALTER TABLE `task_configs` ADD `max_runs_per_hour` integer;
@@ -0,0 +1,711 @@
1
+ {
2
+ "version": "6",
3
+ "dialect": "sqlite",
4
+ "id": "0b04695e-7837-47b0-b25d-14479ebd7fd8",
5
+ "prevId": "d1b9c948-a4d8-4767-ae22-80aefb5ba800",
6
+ "tables": {
7
+ "agent_sessions": {
8
+ "name": "agent_sessions",
9
+ "columns": {
10
+ "id": {
11
+ "name": "id",
12
+ "type": "text",
13
+ "primaryKey": true,
14
+ "notNull": true,
15
+ "autoincrement": false
16
+ },
17
+ "agent_id": {
18
+ "name": "agent_id",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": true,
22
+ "autoincrement": false
23
+ },
24
+ "channel": {
25
+ "name": "channel",
26
+ "type": "text",
27
+ "primaryKey": false,
28
+ "notNull": true,
29
+ "autoincrement": false
30
+ },
31
+ "chat_id": {
32
+ "name": "chat_id",
33
+ "type": "text",
34
+ "primaryKey": false,
35
+ "notNull": true,
36
+ "autoincrement": false
37
+ },
38
+ "thread_id": {
39
+ "name": "thread_id",
40
+ "type": "text",
41
+ "primaryKey": false,
42
+ "notNull": false,
43
+ "autoincrement": false
44
+ },
45
+ "user_id": {
46
+ "name": "user_id",
47
+ "type": "text",
48
+ "primaryKey": false,
49
+ "notNull": false,
50
+ "autoincrement": false
51
+ },
52
+ "pi_session_id": {
53
+ "name": "pi_session_id",
54
+ "type": "text",
55
+ "primaryKey": false,
56
+ "notNull": false,
57
+ "autoincrement": false
58
+ },
59
+ "pi_session_path": {
60
+ "name": "pi_session_path",
61
+ "type": "text",
62
+ "primaryKey": false,
63
+ "notNull": false,
64
+ "autoincrement": false
65
+ },
66
+ "instructions_hash": {
67
+ "name": "instructions_hash",
68
+ "type": "text",
69
+ "primaryKey": false,
70
+ "notNull": false,
71
+ "autoincrement": false
72
+ },
73
+ "model": {
74
+ "name": "model",
75
+ "type": "text",
76
+ "primaryKey": false,
77
+ "notNull": false,
78
+ "autoincrement": false
79
+ },
80
+ "thinking_level": {
81
+ "name": "thinking_level",
82
+ "type": "text",
83
+ "primaryKey": false,
84
+ "notNull": false,
85
+ "autoincrement": false
86
+ },
87
+ "active": {
88
+ "name": "active",
89
+ "type": "integer",
90
+ "primaryKey": false,
91
+ "notNull": true,
92
+ "autoincrement": false,
93
+ "default": true
94
+ },
95
+ "ended_at": {
96
+ "name": "ended_at",
97
+ "type": "integer",
98
+ "primaryKey": false,
99
+ "notNull": false,
100
+ "autoincrement": false
101
+ },
102
+ "created_at": {
103
+ "name": "created_at",
104
+ "type": "integer",
105
+ "primaryKey": false,
106
+ "notNull": true,
107
+ "autoincrement": false
108
+ },
109
+ "updated_at": {
110
+ "name": "updated_at",
111
+ "type": "integer",
112
+ "primaryKey": false,
113
+ "notNull": true,
114
+ "autoincrement": false
115
+ }
116
+ },
117
+ "indexes": {
118
+ "agent_sessions_active_key_idx": {
119
+ "name": "agent_sessions_active_key_idx",
120
+ "columns": [
121
+ "agent_id",
122
+ "channel",
123
+ "chat_id",
124
+ "thread_id",
125
+ "active"
126
+ ],
127
+ "isUnique": false
128
+ }
129
+ },
130
+ "foreignKeys": {},
131
+ "compositePrimaryKeys": {},
132
+ "uniqueConstraints": {},
133
+ "checkConstraints": {}
134
+ },
135
+ "agents": {
136
+ "name": "agents",
137
+ "columns": {
138
+ "id": {
139
+ "name": "id",
140
+ "type": "text",
141
+ "primaryKey": true,
142
+ "notNull": true,
143
+ "autoincrement": false
144
+ },
145
+ "name": {
146
+ "name": "name",
147
+ "type": "text",
148
+ "primaryKey": false,
149
+ "notNull": true,
150
+ "autoincrement": false
151
+ },
152
+ "profile": {
153
+ "name": "profile",
154
+ "type": "text",
155
+ "primaryKey": false,
156
+ "notNull": true,
157
+ "autoincrement": false
158
+ },
159
+ "workspace": {
160
+ "name": "workspace",
161
+ "type": "text",
162
+ "primaryKey": false,
163
+ "notNull": true,
164
+ "autoincrement": false
165
+ },
166
+ "config_json": {
167
+ "name": "config_json",
168
+ "type": "text",
169
+ "primaryKey": false,
170
+ "notNull": true,
171
+ "autoincrement": false
172
+ },
173
+ "system_prompt": {
174
+ "name": "system_prompt",
175
+ "type": "text",
176
+ "primaryKey": false,
177
+ "notNull": true,
178
+ "autoincrement": false,
179
+ "default": "''"
180
+ },
181
+ "created_at": {
182
+ "name": "created_at",
183
+ "type": "integer",
184
+ "primaryKey": false,
185
+ "notNull": true,
186
+ "autoincrement": false
187
+ },
188
+ "updated_at": {
189
+ "name": "updated_at",
190
+ "type": "integer",
191
+ "primaryKey": false,
192
+ "notNull": true,
193
+ "autoincrement": false
194
+ }
195
+ },
196
+ "indexes": {},
197
+ "foreignKeys": {},
198
+ "compositePrimaryKeys": {},
199
+ "uniqueConstraints": {},
200
+ "checkConstraints": {}
201
+ },
202
+ "heartbeats": {
203
+ "name": "heartbeats",
204
+ "columns": {
205
+ "id": {
206
+ "name": "id",
207
+ "type": "text",
208
+ "primaryKey": true,
209
+ "notNull": true,
210
+ "autoincrement": false
211
+ },
212
+ "agent_id": {
213
+ "name": "agent_id",
214
+ "type": "text",
215
+ "primaryKey": false,
216
+ "notNull": true,
217
+ "autoincrement": false
218
+ },
219
+ "session_id": {
220
+ "name": "session_id",
221
+ "type": "text",
222
+ "primaryKey": false,
223
+ "notNull": false,
224
+ "autoincrement": false
225
+ },
226
+ "status": {
227
+ "name": "status",
228
+ "type": "text",
229
+ "primaryKey": false,
230
+ "notNull": true,
231
+ "autoincrement": false
232
+ },
233
+ "prompt": {
234
+ "name": "prompt",
235
+ "type": "text",
236
+ "primaryKey": false,
237
+ "notNull": true,
238
+ "autoincrement": false
239
+ },
240
+ "result": {
241
+ "name": "result",
242
+ "type": "text",
243
+ "primaryKey": false,
244
+ "notNull": false,
245
+ "autoincrement": false
246
+ },
247
+ "error": {
248
+ "name": "error",
249
+ "type": "text",
250
+ "primaryKey": false,
251
+ "notNull": false,
252
+ "autoincrement": false
253
+ },
254
+ "started_at": {
255
+ "name": "started_at",
256
+ "type": "integer",
257
+ "primaryKey": false,
258
+ "notNull": false,
259
+ "autoincrement": false
260
+ },
261
+ "finished_at": {
262
+ "name": "finished_at",
263
+ "type": "integer",
264
+ "primaryKey": false,
265
+ "notNull": false,
266
+ "autoincrement": false
267
+ },
268
+ "created_at": {
269
+ "name": "created_at",
270
+ "type": "integer",
271
+ "primaryKey": false,
272
+ "notNull": true,
273
+ "autoincrement": false
274
+ }
275
+ },
276
+ "indexes": {},
277
+ "foreignKeys": {},
278
+ "compositePrimaryKeys": {},
279
+ "uniqueConstraints": {},
280
+ "checkConstraints": {}
281
+ },
282
+ "messages": {
283
+ "name": "messages",
284
+ "columns": {
285
+ "id": {
286
+ "name": "id",
287
+ "type": "text",
288
+ "primaryKey": true,
289
+ "notNull": true,
290
+ "autoincrement": false
291
+ },
292
+ "agent_id": {
293
+ "name": "agent_id",
294
+ "type": "text",
295
+ "primaryKey": false,
296
+ "notNull": true,
297
+ "autoincrement": false
298
+ },
299
+ "session_id": {
300
+ "name": "session_id",
301
+ "type": "text",
302
+ "primaryKey": false,
303
+ "notNull": false,
304
+ "autoincrement": false
305
+ },
306
+ "channel": {
307
+ "name": "channel",
308
+ "type": "text",
309
+ "primaryKey": false,
310
+ "notNull": true,
311
+ "autoincrement": false
312
+ },
313
+ "direction": {
314
+ "name": "direction",
315
+ "type": "text",
316
+ "primaryKey": false,
317
+ "notNull": true,
318
+ "autoincrement": false
319
+ },
320
+ "sender_id": {
321
+ "name": "sender_id",
322
+ "type": "text",
323
+ "primaryKey": false,
324
+ "notNull": false,
325
+ "autoincrement": false
326
+ },
327
+ "chat_id": {
328
+ "name": "chat_id",
329
+ "type": "text",
330
+ "primaryKey": false,
331
+ "notNull": false,
332
+ "autoincrement": false
333
+ },
334
+ "thread_id": {
335
+ "name": "thread_id",
336
+ "type": "text",
337
+ "primaryKey": false,
338
+ "notNull": false,
339
+ "autoincrement": false
340
+ },
341
+ "content": {
342
+ "name": "content",
343
+ "type": "text",
344
+ "primaryKey": false,
345
+ "notNull": true,
346
+ "autoincrement": false
347
+ },
348
+ "raw_json": {
349
+ "name": "raw_json",
350
+ "type": "text",
351
+ "primaryKey": false,
352
+ "notNull": false,
353
+ "autoincrement": false
354
+ },
355
+ "created_at": {
356
+ "name": "created_at",
357
+ "type": "integer",
358
+ "primaryKey": false,
359
+ "notNull": true,
360
+ "autoincrement": false
361
+ }
362
+ },
363
+ "indexes": {},
364
+ "foreignKeys": {},
365
+ "compositePrimaryKeys": {},
366
+ "uniqueConstraints": {},
367
+ "checkConstraints": {}
368
+ },
369
+ "runtime_kv": {
370
+ "name": "runtime_kv",
371
+ "columns": {
372
+ "key": {
373
+ "name": "key",
374
+ "type": "text",
375
+ "primaryKey": true,
376
+ "notNull": true,
377
+ "autoincrement": false
378
+ },
379
+ "value": {
380
+ "name": "value",
381
+ "type": "text",
382
+ "primaryKey": false,
383
+ "notNull": true,
384
+ "autoincrement": false
385
+ },
386
+ "updated_at": {
387
+ "name": "updated_at",
388
+ "type": "integer",
389
+ "primaryKey": false,
390
+ "notNull": true,
391
+ "autoincrement": false
392
+ }
393
+ },
394
+ "indexes": {},
395
+ "foreignKeys": {},
396
+ "compositePrimaryKeys": {},
397
+ "uniqueConstraints": {},
398
+ "checkConstraints": {}
399
+ },
400
+ "task_configs": {
401
+ "name": "task_configs",
402
+ "columns": {
403
+ "id": {
404
+ "name": "id",
405
+ "type": "text",
406
+ "primaryKey": true,
407
+ "notNull": true,
408
+ "autoincrement": false
409
+ },
410
+ "agent": {
411
+ "name": "agent",
412
+ "type": "text",
413
+ "primaryKey": false,
414
+ "notNull": true,
415
+ "autoincrement": false
416
+ },
417
+ "interval_ms": {
418
+ "name": "interval_ms",
419
+ "type": "integer",
420
+ "primaryKey": false,
421
+ "notNull": true,
422
+ "autoincrement": false
423
+ },
424
+ "prompt": {
425
+ "name": "prompt",
426
+ "type": "text",
427
+ "primaryKey": false,
428
+ "notNull": true,
429
+ "autoincrement": false
430
+ },
431
+ "channel": {
432
+ "name": "channel",
433
+ "type": "text",
434
+ "primaryKey": false,
435
+ "notNull": true,
436
+ "autoincrement": false,
437
+ "default": "'telegram'"
438
+ },
439
+ "chat_id": {
440
+ "name": "chat_id",
441
+ "type": "text",
442
+ "primaryKey": false,
443
+ "notNull": true,
444
+ "autoincrement": false
445
+ },
446
+ "enabled": {
447
+ "name": "enabled",
448
+ "type": "integer",
449
+ "primaryKey": false,
450
+ "notNull": true,
451
+ "autoincrement": false,
452
+ "default": true
453
+ },
454
+ "max_runs_per_hour": {
455
+ "name": "max_runs_per_hour",
456
+ "type": "integer",
457
+ "primaryKey": false,
458
+ "notNull": false,
459
+ "autoincrement": false
460
+ },
461
+ "created_at": {
462
+ "name": "created_at",
463
+ "type": "integer",
464
+ "primaryKey": false,
465
+ "notNull": true,
466
+ "autoincrement": false
467
+ },
468
+ "updated_at": {
469
+ "name": "updated_at",
470
+ "type": "integer",
471
+ "primaryKey": false,
472
+ "notNull": true,
473
+ "autoincrement": false
474
+ }
475
+ },
476
+ "indexes": {},
477
+ "foreignKeys": {},
478
+ "compositePrimaryKeys": {},
479
+ "uniqueConstraints": {},
480
+ "checkConstraints": {}
481
+ },
482
+ "task_runs": {
483
+ "name": "task_runs",
484
+ "columns": {
485
+ "id": {
486
+ "name": "id",
487
+ "type": "text",
488
+ "primaryKey": true,
489
+ "notNull": true,
490
+ "autoincrement": false
491
+ },
492
+ "agent_id": {
493
+ "name": "agent_id",
494
+ "type": "text",
495
+ "primaryKey": false,
496
+ "notNull": true,
497
+ "autoincrement": false
498
+ },
499
+ "task_id": {
500
+ "name": "task_id",
501
+ "type": "text",
502
+ "primaryKey": false,
503
+ "notNull": true,
504
+ "autoincrement": false
505
+ },
506
+ "prompt": {
507
+ "name": "prompt",
508
+ "type": "text",
509
+ "primaryKey": false,
510
+ "notNull": true,
511
+ "autoincrement": false
512
+ },
513
+ "session_id": {
514
+ "name": "session_id",
515
+ "type": "text",
516
+ "primaryKey": false,
517
+ "notNull": false,
518
+ "autoincrement": false
519
+ },
520
+ "status": {
521
+ "name": "status",
522
+ "type": "text",
523
+ "primaryKey": false,
524
+ "notNull": true,
525
+ "autoincrement": false
526
+ },
527
+ "result": {
528
+ "name": "result",
529
+ "type": "text",
530
+ "primaryKey": false,
531
+ "notNull": false,
532
+ "autoincrement": false
533
+ },
534
+ "error": {
535
+ "name": "error",
536
+ "type": "text",
537
+ "primaryKey": false,
538
+ "notNull": false,
539
+ "autoincrement": false
540
+ },
541
+ "started_at": {
542
+ "name": "started_at",
543
+ "type": "integer",
544
+ "primaryKey": false,
545
+ "notNull": false,
546
+ "autoincrement": false
547
+ },
548
+ "finished_at": {
549
+ "name": "finished_at",
550
+ "type": "integer",
551
+ "primaryKey": false,
552
+ "notNull": false,
553
+ "autoincrement": false
554
+ },
555
+ "created_at": {
556
+ "name": "created_at",
557
+ "type": "integer",
558
+ "primaryKey": false,
559
+ "notNull": true,
560
+ "autoincrement": false
561
+ }
562
+ },
563
+ "indexes": {},
564
+ "foreignKeys": {},
565
+ "compositePrimaryKeys": {},
566
+ "uniqueConstraints": {},
567
+ "checkConstraints": {}
568
+ },
569
+ "telegram_chat_agents": {
570
+ "name": "telegram_chat_agents",
571
+ "columns": {
572
+ "id": {
573
+ "name": "id",
574
+ "type": "text",
575
+ "primaryKey": true,
576
+ "notNull": true,
577
+ "autoincrement": false
578
+ },
579
+ "chat_id": {
580
+ "name": "chat_id",
581
+ "type": "text",
582
+ "primaryKey": false,
583
+ "notNull": true,
584
+ "autoincrement": false
585
+ },
586
+ "agent_id": {
587
+ "name": "agent_id",
588
+ "type": "text",
589
+ "primaryKey": false,
590
+ "notNull": true,
591
+ "autoincrement": false
592
+ },
593
+ "enabled": {
594
+ "name": "enabled",
595
+ "type": "integer",
596
+ "primaryKey": false,
597
+ "notNull": true,
598
+ "autoincrement": false,
599
+ "default": true
600
+ },
601
+ "custom_instructions": {
602
+ "name": "custom_instructions",
603
+ "type": "text",
604
+ "primaryKey": false,
605
+ "notNull": true,
606
+ "autoincrement": false,
607
+ "default": "''"
608
+ },
609
+ "created_at": {
610
+ "name": "created_at",
611
+ "type": "integer",
612
+ "primaryKey": false,
613
+ "notNull": true,
614
+ "autoincrement": false
615
+ },
616
+ "updated_at": {
617
+ "name": "updated_at",
618
+ "type": "integer",
619
+ "primaryKey": false,
620
+ "notNull": true,
621
+ "autoincrement": false
622
+ }
623
+ },
624
+ "indexes": {
625
+ "telegram_chat_agents_chat_agent_unique": {
626
+ "name": "telegram_chat_agents_chat_agent_unique",
627
+ "columns": [
628
+ "chat_id",
629
+ "agent_id"
630
+ ],
631
+ "isUnique": true
632
+ }
633
+ },
634
+ "foreignKeys": {},
635
+ "compositePrimaryKeys": {},
636
+ "uniqueConstraints": {},
637
+ "checkConstraints": {}
638
+ },
639
+ "telegram_chats": {
640
+ "name": "telegram_chats",
641
+ "columns": {
642
+ "chat_id": {
643
+ "name": "chat_id",
644
+ "type": "text",
645
+ "primaryKey": true,
646
+ "notNull": true,
647
+ "autoincrement": false
648
+ },
649
+ "title": {
650
+ "name": "title",
651
+ "type": "text",
652
+ "primaryKey": false,
653
+ "notNull": false,
654
+ "autoincrement": false
655
+ },
656
+ "default_agent_id": {
657
+ "name": "default_agent_id",
658
+ "type": "text",
659
+ "primaryKey": false,
660
+ "notNull": false,
661
+ "autoincrement": false
662
+ },
663
+ "instructions": {
664
+ "name": "instructions",
665
+ "type": "text",
666
+ "primaryKey": false,
667
+ "notNull": true,
668
+ "autoincrement": false,
669
+ "default": "''"
670
+ },
671
+ "enabled": {
672
+ "name": "enabled",
673
+ "type": "integer",
674
+ "primaryKey": false,
675
+ "notNull": true,
676
+ "autoincrement": false,
677
+ "default": true
678
+ },
679
+ "created_at": {
680
+ "name": "created_at",
681
+ "type": "integer",
682
+ "primaryKey": false,
683
+ "notNull": true,
684
+ "autoincrement": false
685
+ },
686
+ "updated_at": {
687
+ "name": "updated_at",
688
+ "type": "integer",
689
+ "primaryKey": false,
690
+ "notNull": true,
691
+ "autoincrement": false
692
+ }
693
+ },
694
+ "indexes": {},
695
+ "foreignKeys": {},
696
+ "compositePrimaryKeys": {},
697
+ "uniqueConstraints": {},
698
+ "checkConstraints": {}
699
+ }
700
+ },
701
+ "views": {},
702
+ "enums": {},
703
+ "_meta": {
704
+ "schemas": {},
705
+ "tables": {},
706
+ "columns": {}
707
+ },
708
+ "internal": {
709
+ "indexes": {}
710
+ }
711
+ }
@@ -43,6 +43,13 @@
43
43
  "when": 1779099900000,
44
44
  "tag": "0005_pi_session_path",
45
45
  "breakpoints": true
46
+ },
47
+ {
48
+ "idx": 6,
49
+ "version": "6",
50
+ "when": 1779117599104,
51
+ "tag": "0006_flippant_bruce_banner",
52
+ "breakpoints": true
46
53
  }
47
54
  ]
48
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fickydev/pigent",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Autonomous multi-agent daemon using Pi as core execution engine.",
@@ -1,9 +1,9 @@
1
- import type { LoadedAgentConfig, LoadedConfig, ProfileConfig } from "../config/schemas";
1
+ import type { LoadedAgentConfig, LoadedConfig, LoadedProfileConfig } from "../config/schemas";
2
2
  import type { Repositories } from "../db/repositories";
3
3
 
4
4
  export class AgentRegistry {
5
5
  private readonly agentsById: Map<string, LoadedAgentConfig>;
6
- private readonly profilesById: Map<string, ProfileConfig>;
6
+ private readonly profilesById: Map<string, LoadedProfileConfig>;
7
7
 
8
8
  constructor(
9
9
  private readonly config: LoadedConfig,
@@ -23,7 +23,7 @@ export class AgentRegistry {
23
23
  return [...this.agentsById.values()];
24
24
  }
25
25
 
26
- listProfiles(): ProfileConfig[] {
26
+ listProfiles(): LoadedProfileConfig[] {
27
27
  return [...this.profilesById.values()];
28
28
  }
29
29
 
@@ -31,7 +31,7 @@ export class AgentRegistry {
31
31
  return this.agentsById.get(id) ?? null;
32
32
  }
33
33
 
34
- getProfile(id: string): ProfileConfig | null {
34
+ getProfile(id: string): LoadedProfileConfig | null {
35
35
  return this.profilesById.get(id) ?? null;
36
36
  }
37
37
 
@@ -1,6 +1,6 @@
1
1
  import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
2
2
  import type { InboundMessage, InlineKeyboardButton } from "../channels/types";
3
- import type { LoadedAgentConfig, ModelChoiceConfig, ProfileConfig } from "../config/schemas";
3
+ import type { LoadedAgentConfig, LoadedProfileConfig, ModelChoiceConfig } from "../config/schemas";
4
4
  import type { AgentSessionRow } from "../db/schema";
5
5
  import type { Repositories } from "../db/repositories";
6
6
  import { PiAgentRunner, type PiContextUsage } from "../pi/PiAgentRunner";
@@ -169,7 +169,7 @@ export class BotCommandHandler {
169
169
  ].join("\n");
170
170
  }
171
171
 
172
- private async loadPiStatus(session: AgentSessionRow, agent: LoadedAgentConfig, profile: ProfileConfig | null) {
172
+ private async loadPiStatus(session: AgentSessionRow, agent: LoadedAgentConfig, profile: LoadedProfileConfig | null) {
173
173
  try {
174
174
  return await this.piRunner.status({ agent, profile, session });
175
175
  } catch {
@@ -1,11 +1,12 @@
1
1
  import { readdir, readFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
- import { join, resolve } from "node:path";
3
+ import { dirname, join, resolve } from "node:path";
4
4
  import YAML from "yaml";
5
5
  import {
6
6
  AgentConfigSchema,
7
7
  type LoadedAgentConfig,
8
8
  type LoadedConfig,
9
+ type LoadedProfileConfig,
9
10
  ProfileConfigSchema,
10
11
  RootConfigSchema,
11
12
  } from "./schemas";
@@ -60,7 +61,7 @@ async function loadAgents(rootDir: string): Promise<LoadedAgentConfig[]> {
60
61
  async function loadProfiles(rootDir: string) {
61
62
  const profilesDir = join(rootDir, "profiles");
62
63
  const entries = await readdir(profilesDir, { withFileTypes: true }).catch(() => []);
63
- const profiles = [];
64
+ const profiles: LoadedProfileConfig[] = [];
64
65
 
65
66
  for (const entry of entries) {
66
67
  if (!entry.isFile()) continue;
@@ -68,7 +69,8 @@ async function loadProfiles(rootDir: string) {
68
69
 
69
70
  const configPath = join(profilesDir, entry.name);
70
71
  const parsed = await readYamlFile(configPath);
71
- profiles.push(ProfileConfigSchema.parse(parsed));
72
+ const profile = ProfileConfigSchema.parse(parsed);
73
+ profiles.push({ ...profile, baseDir: dirname(configPath) });
72
74
  }
73
75
 
74
76
  return profiles;
@@ -18,6 +18,7 @@ export const TaskConfigSchema = z.object({
18
18
  prompt: z.string().min(1).default("If no useful action is needed, reply exactly: NOOP."),
19
19
  channel: z.string().default("telegram"),
20
20
  chatId: z.string().optional(),
21
+ maxRunsPerHour: z.number().int().nonnegative().default(12),
21
22
  });
22
23
 
23
24
  export const SchedulerConfigSchema = z.object({
@@ -102,9 +103,13 @@ export type LoadedAgentConfig = AgentConfig & {
102
103
  systemPrompt: string;
103
104
  };
104
105
 
106
+ export type LoadedProfileConfig = ProfileConfig & {
107
+ baseDir: string;
108
+ };
109
+
105
110
  export type LoadedConfig = {
106
111
  agents: LoadedAgentConfig[];
107
- profiles: ProfileConfig[];
112
+ profiles: LoadedProfileConfig[];
108
113
  telegramChats: TelegramChatConfig[];
109
114
  modelChoices: ModelChoiceConfig[];
110
115
  scheduler: SchedulerConfig;
@@ -7,6 +7,8 @@ import type { AgentRunner } from "../agents/AgentRunner";
7
7
  import type { ChannelAdapter } from "../channels/types";
8
8
  import { isTaskDue } from "./taskDue";
9
9
 
10
+ const ONE_HOUR_MS = 3_600_000;
11
+
10
12
  export class Scheduler {
11
13
  private running = false;
12
14
  private tickTimer: ReturnType<typeof setTimeout> | null = null;
@@ -70,6 +72,7 @@ export class Scheduler {
70
72
  prompt: saved.prompt,
71
73
  channel: saved.channel,
72
74
  chatId: saved.chatId,
75
+ maxRunsPerHour: saved.maxRunsPerHour ?? 12,
73
76
  });
74
77
 
75
78
  if (!this.running && this.tasks.length === 1) {
@@ -108,6 +111,7 @@ export class Scheduler {
108
111
  prompt: row.prompt,
109
112
  channel: row.channel,
110
113
  chatId: row.chatId,
114
+ maxRunsPerHour: row.maxRunsPerHour ?? 12,
111
115
  }));
112
116
 
113
117
  return [...configTasks, ...dbTaskConfigs];
@@ -129,6 +133,23 @@ export class Scheduler {
129
133
  const lastRun = await this.repositories.taskRuns.findLatest(task.agent, task.id);
130
134
  if (!isTaskDue(lastRun?.createdAt ?? null, task.intervalMs)) return;
131
135
 
136
+ // Enforce hourly rate limit
137
+ const maxPerHour = task.maxRunsPerHour ?? 12;
138
+ const recentCount = await this.repositories.taskRuns.countRecentNotified(
139
+ task.agent,
140
+ task.id,
141
+ Date.now() - ONE_HOUR_MS,
142
+ );
143
+ if (recentCount >= maxPerHour) {
144
+ logger.warn("task rate limited — max runs per hour reached", {
145
+ taskId: task.id,
146
+ agent: task.agent,
147
+ recentCount,
148
+ maxPerHour,
149
+ });
150
+ return;
151
+ }
152
+
132
153
  const agent = this.registry.getAgent(task.agent);
133
154
  if (!agent) {
134
155
  logger.warn("task references unknown agent, skipping", { taskId: task.id, agent: task.agent });
package/src/db/schema.ts CHANGED
@@ -116,6 +116,7 @@ export const taskConfigs = sqliteTable("task_configs", {
116
116
  channel: text("channel").notNull().default("telegram"),
117
117
  chatId: text("chat_id").notNull(),
118
118
  enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
119
+ maxRunsPerHour: integer("max_runs_per_hour"),
119
120
  createdAt: integer("created_at").notNull(),
120
121
  updatedAt: integer("updated_at").notNull(),
121
122
  });
@@ -8,14 +8,14 @@ import {
8
8
  } from "@earendil-works/pi-coding-agent";
9
9
  import { mkdir } from "node:fs/promises";
10
10
  import { resolve } from "node:path";
11
- import type { LoadedAgentConfig, ProfileConfig } from "../config/schemas";
11
+ import type { LoadedAgentConfig, LoadedProfileConfig } from "../config/schemas";
12
12
  import type { AgentSessionRow } from "../db/schema";
13
13
  import { resolveModelSelection } from "./PiModelResolver";
14
14
  import { loadOrCreatePiSession } from "./PiSessionFactory";
15
15
 
16
16
  export type PiAgentRunInput = {
17
17
  agent: LoadedAgentConfig;
18
- profile: ProfileConfig | null;
18
+ profile: LoadedProfileConfig | null;
19
19
  session: AgentSessionRow;
20
20
  prompt: string;
21
21
  };
@@ -80,11 +80,13 @@ export class PiAgentRunner {
80
80
  compaction: { enabled: false },
81
81
  });
82
82
  const agentDir = getAgentDir();
83
+ const skillPaths = resolveSkillPaths(input.agent, input.profile);
83
84
  const resourceLoader = new DefaultResourceLoader({
84
85
  cwd: workspace,
85
86
  agentDir,
86
87
  settingsManager,
87
88
  systemPromptOverride: () => systemPrompt,
89
+ additionalSkillPaths: skillPaths,
88
90
  });
89
91
  await resourceLoader.reload();
90
92
 
@@ -109,7 +111,23 @@ export class PiAgentRunner {
109
111
  }
110
112
  }
111
113
 
112
- function composeSystemPrompt(agent: LoadedAgentConfig, profile: ProfileConfig | null): string {
114
+ function resolveSkillPaths(agent: LoadedAgentConfig, profile: LoadedProfileConfig | null): string[] {
115
+ const paths: string[] = [];
116
+
117
+ if (profile) {
118
+ for (const s of profile.defaultSkills) {
119
+ paths.push(resolve(profile.baseDir, s));
120
+ }
121
+ }
122
+
123
+ for (const s of agent.skills) {
124
+ paths.push(resolve(agent.baseDir, s));
125
+ }
126
+
127
+ return paths;
128
+ }
129
+
130
+ function composeSystemPrompt(agent: LoadedAgentConfig, profile: LoadedProfileConfig | null): string {
113
131
  return [
114
132
  profile?.instructions,
115
133
  agent.systemPrompt,