@theokit/sdk 5.3.3 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/dist/{agent-CTbU0O1R.d.ts → agent-CjMVbhhY.d.ts} +95 -2
  3. package/dist/{agent-QLwxhXY3.d.cts → agent-DyB_lzrx.d.cts} +95 -2
  4. package/dist/{agent-PNYJ5S2Y.js → agent-G3T7Q3SQ.js} +8 -8
  5. package/dist/{agent-PNYJ5S2Y.js.map → agent-G3T7Q3SQ.js.map} +1 -1
  6. package/dist/{agent-KJLAWKZB.cjs → agent-YWSIHHFM.cjs} +9 -9
  7. package/dist/{agent-KJLAWKZB.cjs.map → agent-YWSIHHFM.cjs.map} +1 -1
  8. package/dist/{chunk-AWINL3ZC.js → chunk-2QKV4CGK.js} +3 -3
  9. package/dist/{chunk-AWINL3ZC.js.map → chunk-2QKV4CGK.js.map} +1 -1
  10. package/dist/{chunk-L4XENLQO.cjs → chunk-2ZLQZHZC.cjs} +4 -4
  11. package/dist/{chunk-L4XENLQO.cjs.map → chunk-2ZLQZHZC.cjs.map} +1 -1
  12. package/dist/{chunk-DWB3CN46.cjs → chunk-43YXGD3P.cjs} +14 -4
  13. package/dist/chunk-43YXGD3P.cjs.map +1 -0
  14. package/dist/{chunk-IQBDR5YZ.cjs → chunk-AYA65JA5.cjs} +5 -5
  15. package/dist/{chunk-IQBDR5YZ.cjs.map → chunk-AYA65JA5.cjs.map} +1 -1
  16. package/dist/{chunk-2HPNT6HH.cjs → chunk-C4ZQPWXJ.cjs} +9 -9
  17. package/dist/chunk-C4ZQPWXJ.cjs.map +1 -0
  18. package/dist/{chunk-Y2KYR2ED.cjs → chunk-DZG77WU7.cjs} +3 -3
  19. package/dist/{chunk-Y2KYR2ED.cjs.map → chunk-DZG77WU7.cjs.map} +1 -1
  20. package/dist/{chunk-K3NH62DG.js → chunk-KZSKIUXZ.js} +7 -7
  21. package/dist/chunk-KZSKIUXZ.js.map +1 -0
  22. package/dist/{chunk-ZITYGJOD.js → chunk-M3ZGRGOG.js} +3 -3
  23. package/dist/{chunk-ZITYGJOD.js.map → chunk-M3ZGRGOG.js.map} +1 -1
  24. package/dist/{chunk-RP3VXJMA.js → chunk-NSLHPAC7.js} +14 -4
  25. package/dist/chunk-NSLHPAC7.js.map +1 -0
  26. package/dist/{chunk-DLFWMJE3.js → chunk-O7L7M42F.js} +3 -3
  27. package/dist/{chunk-DLFWMJE3.js.map → chunk-O7L7M42F.js.map} +1 -1
  28. package/dist/{chunk-363RBGIR.js → chunk-ODBID5TG.js} +63 -27
  29. package/dist/chunk-ODBID5TG.js.map +1 -0
  30. package/dist/{chunk-7L3MIW2F.js → chunk-S6B5XYC3.js} +4 -4
  31. package/dist/chunk-S6B5XYC3.js.map +1 -0
  32. package/dist/{chunk-Y46VI73J.cjs → chunk-UKJJRU7C.cjs} +132 -96
  33. package/dist/chunk-UKJJRU7C.cjs.map +1 -0
  34. package/dist/{chunk-65UMH5V3.cjs → chunk-VY6NKMOA.cjs} +6 -6
  35. package/dist/chunk-VY6NKMOA.cjs.map +1 -0
  36. package/dist/{compact-session-BHTPCIHA.cjs → compact-session-7FMYJXQ7.cjs} +11 -11
  37. package/dist/{compact-session-BHTPCIHA.cjs.map → compact-session-7FMYJXQ7.cjs.map} +1 -1
  38. package/dist/{compact-session-W4WLJTHA.js → compact-session-YSEK7NQF.js} +3 -3
  39. package/dist/{compact-session-W4WLJTHA.js.map → compact-session-YSEK7NQF.js.map} +1 -1
  40. package/dist/context-3HPU754C.js +6 -0
  41. package/dist/{context-BUIE52VH.js.map → context-3HPU754C.js.map} +1 -1
  42. package/dist/{context-FCCJQ3FN.cjs → context-JTGSBJT6.cjs} +7 -7
  43. package/dist/{context-FCCJQ3FN.cjs.map → context-JTGSBJT6.cjs.map} +1 -1
  44. package/dist/{cron-Dz8AOpy3.d.cts → cron-Bgivg88c.d.cts} +1 -1
  45. package/dist/{cron-Cl8FrK3D.d.ts → cron-CNUa7PDo.d.ts} +1 -1
  46. package/dist/cron.cjs +8 -8
  47. package/dist/cron.d.cts +2 -2
  48. package/dist/cron.d.ts +2 -2
  49. package/dist/cron.js +7 -7
  50. package/dist/eval.cjs +7 -7
  51. package/dist/eval.js +6 -6
  52. package/dist/index.cjs +30 -30
  53. package/dist/index.d.cts +3 -3
  54. package/dist/index.d.ts +3 -3
  55. package/dist/index.js +9 -9
  56. package/dist/{inject-session-SUM6TDJF.js → inject-session-J4LBWAPH.js} +3 -3
  57. package/dist/{inject-session-SUM6TDJF.js.map → inject-session-J4LBWAPH.js.map} +1 -1
  58. package/dist/{inject-session-W6A46J3E.cjs → inject-session-PC73TMP2.cjs} +4 -4
  59. package/dist/{inject-session-W6A46J3E.cjs.map → inject-session-PC73TMP2.cjs.map} +1 -1
  60. package/dist/internal/llm/tool-result-content.d.ts +1 -1
  61. package/dist/internal/persistence/index.cjs +4 -4
  62. package/dist/internal/persistence/index.js +1 -1
  63. package/dist/internal/runtime/hooks/hooks-executor.d.ts +35 -1
  64. package/dist/internal/runtime/hooks/hooks-source.d.ts +10 -0
  65. package/dist/subagents-loader-AIVDQ2D5.js +7 -0
  66. package/dist/{subagents-loader-PAZVOZHI.js.map → subagents-loader-AIVDQ2D5.js.map} +1 -1
  67. package/dist/subagents-loader-DN4LETGL.cjs +16 -0
  68. package/dist/{subagents-loader-7G76XOZU.cjs.map → subagents-loader-DN4LETGL.cjs.map} +1 -1
  69. package/dist/subagents-loader.cjs +3 -3
  70. package/dist/subagents-loader.d.cts +1 -1
  71. package/dist/subagents-loader.d.ts +1 -1
  72. package/dist/subagents-loader.js +2 -2
  73. package/dist/types/agent.d.ts +25 -2
  74. package/dist/types/hooks.d.ts +70 -0
  75. package/docs/error-codes.md +4 -4
  76. package/package.json +1 -1
  77. package/dist/chunk-2HPNT6HH.cjs.map +0 -1
  78. package/dist/chunk-363RBGIR.js.map +0 -1
  79. package/dist/chunk-65UMH5V3.cjs.map +0 -1
  80. package/dist/chunk-7L3MIW2F.js.map +0 -1
  81. package/dist/chunk-DWB3CN46.cjs.map +0 -1
  82. package/dist/chunk-K3NH62DG.js.map +0 -1
  83. package/dist/chunk-RP3VXJMA.js.map +0 -1
  84. package/dist/chunk-Y46VI73J.cjs.map +0 -1
  85. package/dist/context-BUIE52VH.js +0 -6
  86. package/dist/subagents-loader-7G76XOZU.cjs +0 -16
  87. package/dist/subagents-loader-PAZVOZHI.js +0 -7
package/CHANGELOG.md CHANGED
@@ -1,5 +1,106 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#639](https://github.com/usetheokit/theokit-sdk/pull/639) [`4f2e86c`](https://github.com/usetheokit/theokit-sdk/commit/4f2e86cc8bfa0e7947d9e2d11c8f20e0e35910a1) Thanks [@usetheodev](https://github.com/usetheodev)! - A hook approval request carries the identity a consumer stored ([#637](https://github.com/usetheokit/theokit-sdk/issues/637))
8
+
9
+ `local.hooks.approve` let a consumer refuse a hook. It did not let one **approve** a particular
10
+ hook, because the two sides could not compute the same fingerprint — so the only workable policy
11
+ was to refuse everything, and a hook a user deliberately wrote could not be run.
12
+
13
+ `HookApprovalRequest` now carries two more fields:
14
+
15
+ ```ts
16
+ local: {
17
+ hooks: {
18
+ approve: ({ command, sourceEvent, matcher, timeoutMs }) =>
19
+ myStore.has(hash({ command, event: sourceEvent, matcher, timeoutMs })),
20
+ },
21
+ }
22
+ ```
23
+
24
+ - **`timeoutMs`** — the timeout the runtime **will apply**, default already resolved. A config
25
+ that omits `timeout` still runs under one (30s); reporting `undefined` there would make every
26
+ consumer reimplement this package's default to reproduce a stored hash, and a default duplicated
27
+ across a boundary is a default that drifts.
28
+ - **`sourceEvent`** — the event key exactly as the config file spelled it (`PreToolUse`), which is
29
+ not `event` (`preToolUse`). An approval is taken against the file shown to the user, so the
30
+ stored fingerprint is over the file's vocabulary. The literal key rather than a published
31
+ mapping table, because a mapping is a second thing to keep in step with the first.
32
+
33
+ Both are **required**, not optional: `parseClaudeCodeCommand` is this package's only producer of a
34
+ hook command, so every hook reaching the gate has both. An optional field would hand every
35
+ consumer a fallback branch for a case that cannot occur, and in a security gate each extra branch
36
+ is a second answer to one question.
37
+
38
+ Matching on the subset that survived was the tempting fix and is the wrong one — it would be a
39
+ second identity answering the same question, which is how one ends up approving what the other
40
+ refuses.
41
+
42
+ Reading a request is unaffected; nothing that compiled before stops compiling.
43
+
44
+ ## 5.4.0
45
+
46
+ ### Minor Changes
47
+
48
+ - [#633](https://github.com/usetheokit/theokit-sdk/pull/633) [`ee1e174`](https://github.com/usetheokit/theokit-sdk/commit/ee1e1748f4d0c5f2d4efdacd68a49d2a0b5e09cb) Thanks [@usetheodev](https://github.com/usetheodev)! - The native config root can decline a surface, and a consumer can refuse a hook before it is spawned ([#631](https://github.com/usetheokit/theokit-sdk/issues/631))
49
+
50
+ Two additions, both absent-means-today's-behaviour.
51
+
52
+ ## `{ kind: "theokit", import: [...] }` in `compatSources`
53
+
54
+ `projectConfigRoots` prepended `.theokit/` unconditionally. A **foreign** dialect could already declare which surfaces it contributes; the SDK's own root could declare nothing and contributed all four, always.
55
+
56
+ That asymmetry had a measured cost. `hookConfigCandidates` reads `settings.json` from every root, so a consumer keeping its own configuration in `.theokit/settings.json` had that file's `hooks` key executed by this package — with no gate, and twice when the consumer also ran them. Measured by `usetheoai-lab/TheoCode`: unapproved fired once, approved fired twice.
57
+
58
+ The consumer could not opt out: `settingSources` grants a dialect per SOURCE, not per surface, so dropping `claude-code` to avoid its hooks would also drop its skills, agents and rules — the reason an adopter can use this SDK without migrating anything.
59
+
60
+ ```ts
61
+ local: { compatSources: [{ kind: "theokit", import: ["skills", "subagents"] }] }
62
+ // hooks omitted on purpose — this package will not read them from .theokit/
63
+ ```
64
+
65
+ Declaring nothing keeps every surface, exactly as before. A bare `"theokit"` admits everything, matching the rule foreign kinds already follow. It rides on `compatSources`, which already reaches all four surfaces, so no new parameter is threaded anywhere.
66
+
67
+ **Deliberately not included: renaming the directory.** `theokitConfigRoot` is also the DATA root — `agent-registry-store.ts` builds `registry.json`'s path from it directly — so a `dirName` override would move persisted state. Different change, different blast radius, its own migration question.
68
+
69
+ ## `local.hooks.approve`
70
+
71
+ ```ts
72
+ local: { hooks: { approve: (req) => myFingerprintStore.has(req.command) } }
73
+ ```
74
+
75
+ Consulted at the single point a hook is spawned, for every root — including a foreign dialect's, which the declaration above cannot reach. The request carries the command text, the event, and the file it was declared in, because a consumer's fingerprint is over the command and the source is what separates its own configuration from a dialect it merely imported.
76
+
77
+ Absent means run: a gate defaulting to refusal would disable every hook in the ecosystem on an upgrade.
78
+
79
+ **A refused hook resolves as if it were not configured — it does not deny the operation.** `preRun` and `preToolUse` decisions can block what they attach to, so treating "not approved" as a denial would make an unapproved hook worse than an absent one. The consumer asked for the command not to run, not for the work to stop.
80
+
81
+ ### Patch Changes
82
+
83
+ - [#633](https://github.com/usetheokit/theokit-sdk/pull/633) [`ee1e174`](https://github.com/usetheokit/theokit-sdk/commit/ee1e1748f4d0c5f2d4efdacd68a49d2a0b5e09cb) Thanks [@usetheodev](https://github.com/usetheodev)! - The image-in-tool-results refusal names the tool call whose result carried the image ([#629](https://github.com/usetheokit/theokit-sdk/issues/629))
84
+
85
+ Before:
86
+
87
+ ```
88
+ provider "openai-responses" does not support image content in tool results
89
+ ```
90
+
91
+ After:
92
+
93
+ ```
94
+ provider "openai-responses" does not support image content in tool results —
95
+ the result of tool call call_42 carried an image, and this wire carries tool results as text only
96
+ ```
97
+
98
+ Accurate before, and unhelpful. The person who hit it had attached an image and asked a question about it; they never asked for a tool, so a message naming *tool results* pointed at a surface they had not used. Measured downstream (`usetheoai-lab/TheoCode#133`): the turn that failed made two tool calls and the five that succeeded made none — asking for *dimensions and colours* provoked a tool call, asking for *one word* did not, which is why it looked intermittent and was not.
99
+
100
+ The **id** rather than the tool's name, and stated rather than quietly settled for: `LlmToolResultPart` carries `toolUseId` only — the name lives on the matching `LlmToolCallPart` in an earlier message, and the four sites that construct a tool result do not have it either. The id is the correlation key that exists at the call site, it is what a reader can find in a transcript, and it does not pretend to be a name.
101
+
102
+ The parameter is optional, so nothing that calls this helper with two arguments changes.
103
+
3
104
  ## 5.3.3
4
105
 
5
106
  ### Patch Changes
@@ -348,6 +348,77 @@ interface BudgetTracker {
348
348
  nextIteration?(): void;
349
349
  }
350
350
 
351
+ /**
352
+ * Hook contract types.
353
+ *
354
+ * Owner: `internal/runtime/hooks/` (2 of 3 importers). Derived from the import graph, not declared —
355
+ * `hooks-executor.ts` re-exports every name here, and `types/agent.ts` reads only the gate.
356
+ *
357
+ * A LEAF: it imports nothing, so anything may depend on it without closing a cycle. That is the
358
+ * whole reason it exists as its own file. The first version of #631 put
359
+ * `import("../internal/runtime/hooks/hooks-executor.js").HookApprovalGate` inline in
360
+ * `types/agent.ts`, which made the public contract depend on an internal module and produced
361
+ * thirteen failing cycle assertions — the exact defect `types/plugin.ts` documents at its own head
362
+ * ("the public contract sits above the DIP boundary").
363
+ *
364
+ * `HookEvent` is DECLARED here and re-exported by `internal/runtime/hooks/hooks-executor.ts`, so
365
+ * that pair has one definition. `internal/runtime/hooks/hooks-source.ts` still carries its own copy;
366
+ * consolidating that third one is the same class of problem as #586 and is deliberately not folded
367
+ * into this change.
368
+ *
369
+ * @public
370
+ */
371
+ /** The five lifecycle events the runtime actually fires. */
372
+ type HookEvent = "preRun" | "postRun" | "preToolUse" | "postToolUse" | "stop";
373
+ /**
374
+ * #631 — what the consumer is shown when asked to approve a hook.
375
+ *
376
+ * The command TEXT and the file it was declared in, because neither alone answers the question. A
377
+ * consumer's fingerprint is over the command; which file it came from is what separates its own
378
+ * configuration from a foreign dialect it merely imported, and those deserve different answers.
379
+ *
380
+ * @public
381
+ */
382
+ interface HookApprovalRequest {
383
+ readonly command: string;
384
+ readonly event: HookEvent;
385
+ readonly sourcePath?: string;
386
+ readonly matcher?: string;
387
+ /**
388
+ * #637 — the timeout the runtime WILL apply, in milliseconds, default already resolved.
389
+ *
390
+ * A config that omits `timeout` still runs under one (30s). Reporting `undefined` there would
391
+ * make a consumer reimplement this package's default in order to compute the same fingerprint,
392
+ * and a default duplicated across a boundary is a default that drifts.
393
+ */
394
+ readonly timeoutMs: number;
395
+ /**
396
+ * #637 — the event key exactly as the config file spelled it (`PreToolUse`), which is NOT
397
+ * `event` (`preToolUse`).
398
+ *
399
+ * A consumer's approval is taken against the file it showed the user, so its stored fingerprint
400
+ * is over the file's vocabulary. Without this the two sides hash different strings and no hook
401
+ * can ever be approved — only refused. The literal key rather than a published mapping table,
402
+ * because a mapping is a second thing to keep in step with the first.
403
+ *
404
+ * `string` and not a union of the keys this SDK currently recognises: the vocabulary belongs to
405
+ * whichever dialect declared the hook, so narrowing it would make adding a dialect a breaking
406
+ * change to this public type. A consumer hashes it; it does not need to switch on it.
407
+ */
408
+ readonly sourceEvent: string;
409
+ }
410
+ /**
411
+ * #631 — a consumer's decision point before this package spawns a hook.
412
+ *
413
+ * Absent means run, which is what every consumer gets today: a gate that defaulted to refusing
414
+ * would disable every hook in the ecosystem on an upgrade.
415
+ *
416
+ * @public
417
+ */
418
+ interface HookApprovalGate {
419
+ readonly approve?: (request: HookApprovalRequest) => boolean | Promise<boolean>;
420
+ }
421
+
351
422
  /**
352
423
  * Owner: `internal/memory/storage/` (1 of 2 importers). Derived from the import graph, not
353
424
  * declared — `tests/lint/types-name-their-owner.test.ts` re-derives it.
@@ -971,7 +1042,7 @@ type SettingSource = "project" | "user" | "team" | "mdm" | "plugins" | "all";
971
1042
  *
972
1043
  * @public
973
1044
  */
974
- type CompatSource = "claude-code" | CompatSourceAdapter;
1045
+ type CompatSource = "claude-code" | "theokit" | CompatSourceAdapter;
975
1046
  /**
976
1047
  * A surface a foreign configuration source may be admitted to.
977
1048
  *
@@ -1001,7 +1072,17 @@ type CompatSurface = "hooks" | "plugins" | "skills" | "subagents";
1001
1072
  * @public
1002
1073
  */
1003
1074
  interface CompatSourceAdapter {
1004
- readonly kind: "claude-code";
1075
+ /**
1076
+ * `"theokit"` names THIS package's own root (#631), in the same vocabulary a foreign dialect
1077
+ * uses. Until it was accepted here, `.theokit/` contributed every surface unconditionally while a
1078
+ * foreign one could declare which surfaces it wanted — an asymmetry with a measured cost: a
1079
+ * consumer keeping its own configuration in `.theokit/settings.json` had that file's `hooks` key
1080
+ * executed by this package, and `settingSources` gave it no way to decline, because it grants per
1081
+ * SOURCE and not per surface.
1082
+ *
1083
+ * Declaring nothing keeps today's behaviour exactly: the native root contributes everything.
1084
+ */
1085
+ readonly kind: "claude-code" | "theokit";
1005
1086
  readonly import?: readonly CompatSurface[];
1006
1087
  }
1007
1088
  /**
@@ -1069,6 +1150,18 @@ interface LocalOptions {
1069
1150
  * wrote for this runtime.
1070
1151
  */
1071
1152
  compatSources?: CompatSource[];
1153
+ /**
1154
+ * #631 — the consumer's chance to refuse a hook command before this package spawns it.
1155
+ *
1156
+ * Absent means run, which is what every consumer gets today. It is consulted at the single point
1157
+ * a hook is executed, for every root — including a foreign dialect's, which is the case
1158
+ * {@link CompatSourceAdapter} cannot reach: a consumer that wants `.claude/`'s skills, agents and
1159
+ * rules cannot decline only its hooks, because `settingSources` grants per source.
1160
+ *
1161
+ * A refused hook resolves as if it were not configured. It does NOT deny the operation the hook
1162
+ * attached to — the consumer asked for the command not to run, not for the work to stop.
1163
+ */
1164
+ hooks?: HookApprovalGate;
1072
1165
  sandboxOptions?: {
1073
1166
  enabled: boolean;
1074
1167
  };
@@ -348,6 +348,77 @@ interface BudgetTracker {
348
348
  nextIteration?(): void;
349
349
  }
350
350
 
351
+ /**
352
+ * Hook contract types.
353
+ *
354
+ * Owner: `internal/runtime/hooks/` (2 of 3 importers). Derived from the import graph, not declared —
355
+ * `hooks-executor.ts` re-exports every name here, and `types/agent.ts` reads only the gate.
356
+ *
357
+ * A LEAF: it imports nothing, so anything may depend on it without closing a cycle. That is the
358
+ * whole reason it exists as its own file. The first version of #631 put
359
+ * `import("../internal/runtime/hooks/hooks-executor.js").HookApprovalGate` inline in
360
+ * `types/agent.ts`, which made the public contract depend on an internal module and produced
361
+ * thirteen failing cycle assertions — the exact defect `types/plugin.ts` documents at its own head
362
+ * ("the public contract sits above the DIP boundary").
363
+ *
364
+ * `HookEvent` is DECLARED here and re-exported by `internal/runtime/hooks/hooks-executor.ts`, so
365
+ * that pair has one definition. `internal/runtime/hooks/hooks-source.ts` still carries its own copy;
366
+ * consolidating that third one is the same class of problem as #586 and is deliberately not folded
367
+ * into this change.
368
+ *
369
+ * @public
370
+ */
371
+ /** The five lifecycle events the runtime actually fires. */
372
+ type HookEvent = "preRun" | "postRun" | "preToolUse" | "postToolUse" | "stop";
373
+ /**
374
+ * #631 — what the consumer is shown when asked to approve a hook.
375
+ *
376
+ * The command TEXT and the file it was declared in, because neither alone answers the question. A
377
+ * consumer's fingerprint is over the command; which file it came from is what separates its own
378
+ * configuration from a foreign dialect it merely imported, and those deserve different answers.
379
+ *
380
+ * @public
381
+ */
382
+ interface HookApprovalRequest {
383
+ readonly command: string;
384
+ readonly event: HookEvent;
385
+ readonly sourcePath?: string;
386
+ readonly matcher?: string;
387
+ /**
388
+ * #637 — the timeout the runtime WILL apply, in milliseconds, default already resolved.
389
+ *
390
+ * A config that omits `timeout` still runs under one (30s). Reporting `undefined` there would
391
+ * make a consumer reimplement this package's default in order to compute the same fingerprint,
392
+ * and a default duplicated across a boundary is a default that drifts.
393
+ */
394
+ readonly timeoutMs: number;
395
+ /**
396
+ * #637 — the event key exactly as the config file spelled it (`PreToolUse`), which is NOT
397
+ * `event` (`preToolUse`).
398
+ *
399
+ * A consumer's approval is taken against the file it showed the user, so its stored fingerprint
400
+ * is over the file's vocabulary. Without this the two sides hash different strings and no hook
401
+ * can ever be approved — only refused. The literal key rather than a published mapping table,
402
+ * because a mapping is a second thing to keep in step with the first.
403
+ *
404
+ * `string` and not a union of the keys this SDK currently recognises: the vocabulary belongs to
405
+ * whichever dialect declared the hook, so narrowing it would make adding a dialect a breaking
406
+ * change to this public type. A consumer hashes it; it does not need to switch on it.
407
+ */
408
+ readonly sourceEvent: string;
409
+ }
410
+ /**
411
+ * #631 — a consumer's decision point before this package spawns a hook.
412
+ *
413
+ * Absent means run, which is what every consumer gets today: a gate that defaulted to refusing
414
+ * would disable every hook in the ecosystem on an upgrade.
415
+ *
416
+ * @public
417
+ */
418
+ interface HookApprovalGate {
419
+ readonly approve?: (request: HookApprovalRequest) => boolean | Promise<boolean>;
420
+ }
421
+
351
422
  /**
352
423
  * Owner: `internal/memory/storage/` (1 of 2 importers). Derived from the import graph, not
353
424
  * declared — `tests/lint/types-name-their-owner.test.ts` re-derives it.
@@ -971,7 +1042,7 @@ type SettingSource = "project" | "user" | "team" | "mdm" | "plugins" | "all";
971
1042
  *
972
1043
  * @public
973
1044
  */
974
- type CompatSource = "claude-code" | CompatSourceAdapter;
1045
+ type CompatSource = "claude-code" | "theokit" | CompatSourceAdapter;
975
1046
  /**
976
1047
  * A surface a foreign configuration source may be admitted to.
977
1048
  *
@@ -1001,7 +1072,17 @@ type CompatSurface = "hooks" | "plugins" | "skills" | "subagents";
1001
1072
  * @public
1002
1073
  */
1003
1074
  interface CompatSourceAdapter {
1004
- readonly kind: "claude-code";
1075
+ /**
1076
+ * `"theokit"` names THIS package's own root (#631), in the same vocabulary a foreign dialect
1077
+ * uses. Until it was accepted here, `.theokit/` contributed every surface unconditionally while a
1078
+ * foreign one could declare which surfaces it wanted — an asymmetry with a measured cost: a
1079
+ * consumer keeping its own configuration in `.theokit/settings.json` had that file's `hooks` key
1080
+ * executed by this package, and `settingSources` gave it no way to decline, because it grants per
1081
+ * SOURCE and not per surface.
1082
+ *
1083
+ * Declaring nothing keeps today's behaviour exactly: the native root contributes everything.
1084
+ */
1085
+ readonly kind: "claude-code" | "theokit";
1005
1086
  readonly import?: readonly CompatSurface[];
1006
1087
  }
1007
1088
  /**
@@ -1069,6 +1150,18 @@ interface LocalOptions {
1069
1150
  * wrote for this runtime.
1070
1151
  */
1071
1152
  compatSources?: CompatSource[];
1153
+ /**
1154
+ * #631 — the consumer's chance to refuse a hook command before this package spawns it.
1155
+ *
1156
+ * Absent means run, which is what every consumer gets today. It is consulted at the single point
1157
+ * a hook is executed, for every root — including a foreign dialect's, which is the case
1158
+ * {@link CompatSourceAdapter} cannot reach: a consumer that wants `.claude/`'s skills, agents and
1159
+ * rules cannot decline only its hooks, because `settingSources` grants per source.
1160
+ *
1161
+ * A refused hook resolves as if it were not configured. It does NOT deny the operation the hook
1162
+ * attached to — the consumer asked for the command not to run, not for the work to stop.
1163
+ */
1164
+ hooks?: HookApprovalGate;
1072
1165
  sandboxOptions?: {
1073
1166
  enabled: boolean;
1074
1167
  };
@@ -1,16 +1,16 @@
1
- export { Agent } from './chunk-363RBGIR.js';
1
+ export { Agent } from './chunk-ODBID5TG.js';
2
2
  import './chunk-K2VMFZQ5.js';
3
- import './chunk-AWINL3ZC.js';
3
+ import './chunk-2QKV4CGK.js';
4
4
  import './chunk-K2BQQ445.js';
5
5
  import './chunk-7SZAV6QG.js';
6
- import './chunk-7L3MIW2F.js';
6
+ import './chunk-S6B5XYC3.js';
7
7
  import './chunk-2SFBB54R.js';
8
8
  import './chunk-55GB6JYQ.js';
9
9
  import './chunk-H73MEMQB.js';
10
10
  import './chunk-OR6XIWLB.js';
11
11
  import './chunk-SADXXGWU.js';
12
12
  import './chunk-AAEZSPTC.js';
13
- import './chunk-K3NH62DG.js';
13
+ import './chunk-KZSKIUXZ.js';
14
14
  import './chunk-UBR5PWD7.js';
15
15
  import './chunk-2XC6KDJM.js';
16
16
  import './chunk-XB4GI5RZ.js';
@@ -41,8 +41,8 @@ import './chunk-TA3K7SBK.js';
41
41
  import './chunk-Q5EWJPRY.js';
42
42
  import './chunk-EIQFAOFD.js';
43
43
  import './chunk-3JHIFQ4I.js';
44
- import './chunk-DLFWMJE3.js';
45
- import './chunk-RP3VXJMA.js';
44
+ import './chunk-O7L7M42F.js';
45
+ import './chunk-NSLHPAC7.js';
46
46
  import './chunk-JNAA4G4H.js';
47
47
  import './chunk-6M2OIS4Y.js';
48
48
  import './chunk-R7WIIPUR.js';
@@ -54,5 +54,5 @@ import './chunk-V22DZIXO.js';
54
54
  import './chunk-NJWYQWDL.js';
55
55
  import './chunk-ALUN2B4W.js';
56
56
  import './chunk-CZJ6Q7CW.js';
57
- //# sourceMappingURL=agent-PNYJ5S2Y.js.map
58
- //# sourceMappingURL=agent-PNYJ5S2Y.js.map
57
+ //# sourceMappingURL=agent-G3T7Q3SQ.js.map
58
+ //# sourceMappingURL=agent-G3T7Q3SQ.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"agent-PNYJ5S2Y.js"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"agent-G3T7Q3SQ.js"}
@@ -1,18 +1,18 @@
1
1
  'use strict';
2
2
 
3
- var chunkY46VI73J_cjs = require('./chunk-Y46VI73J.cjs');
3
+ var chunkUKJJRU7C_cjs = require('./chunk-UKJJRU7C.cjs');
4
4
  require('./chunk-KVSAY6NZ.cjs');
5
- require('./chunk-Y2KYR2ED.cjs');
5
+ require('./chunk-DZG77WU7.cjs');
6
6
  require('./chunk-BUUUWQMB.cjs');
7
7
  require('./chunk-Z2JFX372.cjs');
8
- require('./chunk-65UMH5V3.cjs');
8
+ require('./chunk-VY6NKMOA.cjs');
9
9
  require('./chunk-BV2MWEMV.cjs');
10
10
  require('./chunk-D6POWE7E.cjs');
11
11
  require('./chunk-GHX4P3V2.cjs');
12
12
  require('./chunk-TMKTSYDS.cjs');
13
13
  require('./chunk-NQTNSHSB.cjs');
14
14
  require('./chunk-7FDKWDA2.cjs');
15
- require('./chunk-2HPNT6HH.cjs');
15
+ require('./chunk-C4ZQPWXJ.cjs');
16
16
  require('./chunk-FUSDYC2B.cjs');
17
17
  require('./chunk-YJNYDJ2T.cjs');
18
18
  require('./chunk-24SYBZPL.cjs');
@@ -43,8 +43,8 @@ require('./chunk-BJUJT5ED.cjs');
43
43
  require('./chunk-ZF2LDKQQ.cjs');
44
44
  require('./chunk-HCT4HPCL.cjs');
45
45
  require('./chunk-JLRLCBJ4.cjs');
46
- require('./chunk-IQBDR5YZ.cjs');
47
- require('./chunk-DWB3CN46.cjs');
46
+ require('./chunk-AYA65JA5.cjs');
47
+ require('./chunk-43YXGD3P.cjs');
48
48
  require('./chunk-HW7SEELD.cjs');
49
49
  require('./chunk-ATT276RD.cjs');
50
50
  require('./chunk-3EE6LVWT.cjs');
@@ -61,7 +61,7 @@ require('./chunk-6LHQPOMI.cjs');
61
61
 
62
62
  Object.defineProperty(exports, "Agent", {
63
63
  enumerable: true,
64
- get: function () { return chunkY46VI73J_cjs.Agent; }
64
+ get: function () { return chunkUKJJRU7C_cjs.Agent; }
65
65
  });
66
- //# sourceMappingURL=agent-KJLAWKZB.cjs.map
67
- //# sourceMappingURL=agent-KJLAWKZB.cjs.map
66
+ //# sourceMappingURL=agent-YWSIHHFM.cjs.map
67
+ //# sourceMappingURL=agent-YWSIHHFM.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"agent-KJLAWKZB.cjs"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"agent-YWSIHHFM.cjs"}
@@ -46,7 +46,7 @@ function persistTurnToTranscript(store, loc, sessionId, turn, onCompact) {
46
46
  const count = (recordCounts.get(key) ?? 0) + 1;
47
47
  recordCounts.set(key, count);
48
48
  if (turn.autoCompact !== void 0) {
49
- const { autoCompactIfNeeded } = await import('./compact-session-W4WLJTHA.js');
49
+ const { autoCompactIfNeeded } = await import('./compact-session-YSEK7NQF.js');
50
50
  const fired = await autoCompactIfNeeded({
51
51
  store,
52
52
  loc,
@@ -105,5 +105,5 @@ function enqueueSessionWrite(cwd, agentId, fn) {
105
105
  }
106
106
 
107
107
  export { appendSessionMessage, clearSession, discardSession, enqueueSessionWrite, flushSessionWrites, getSessionMessages, hydrateSession, persistTurnToTranscript };
108
- //# sourceMappingURL=chunk-AWINL3ZC.js.map
109
- //# sourceMappingURL=chunk-AWINL3ZC.js.map
108
+ //# sourceMappingURL=chunk-2QKV4CGK.js.map
109
+ //# sourceMappingURL=chunk-2QKV4CGK.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/internal/session/agent-session.ts"],"names":[],"mappings":"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","file":"chunk-AWINL3ZC.js","sourcesContent":["import type { SessionStore } from \"../../types/session-store.js\";\nimport {\n type PersistTurnInput,\n persistTurn,\n readSessionMessages,\n type TranscriptLocation,\n} from \"./agent-session-store.js\";\n\n/**\n * Per-agent conversation history kept across runs (and across `Agent.resume()`\n * within the same process). Lets the fixture responder recall prior facts when\n * the user asks a follow-up question.\n *\n * SE40 — disk persistence IS the native Claude-shaped `.jsonl` transcript. The\n * in-memory cache holds the narrowed `SessionMessage[]` (user/assistant text) so\n * a send can read `priorMessages` synchronously; the whole rich turn (user +\n * assistant + tool blocks) lands on disk once per send via {@link persistTurnToTranscript}\n * (fed by `run.conversation()`). Hydration on resume reconstructs the session by\n * walking the transcript DAG (`readSessionMessages`).\n *\n * @internal\n */\n\n// `SessionMessage` lives in `./session-types.ts` (leaf types file). Re-exported\n// for back-compat with downstream importers that pulled it from here.\nexport type { SessionMessage } from \"./types.js\";\n\nimport type { SessionMessage } from \"./types.js\";\n\n// M75 — the cache lives in a leaf; see session-cache.ts for the reason (cycle broken by extraction).\nexport {\n hydratedKeys,\n invalidateSessionCache,\n sessions,\n transcriptKey,\n} from \"./session-cache.js\";\n\nimport { diag } from \"../diagnostics.js\";\nimport { globalSingleton } from \"../global-singleton.js\";\nimport { hydratedKeys, sessions, transcriptKey } from \"./session-cache.js\";\n\nconst pendingWrites = new Map<string, Promise<void>>();\nconst recordCounts = new Map<string, number>();\n\n/**\n * Append a session message to the in-memory cache only. Disk persistence for the\n * whole turn happens once per send via {@link persistTurnToTranscript}; the cache\n * feeds `priorMessages` / `onBeforeSend.previousMessageCount` synchronously.\n *\n * @internal\n */\nexport function appendSessionMessage(agentId: string, message: SessionMessage): void {\n const existing = sessions.get(agentId) ?? [];\n existing.push(message);\n // `delete` + `set` reinserts at the END: a JS `Map` preserves insertion order, so the first key\n // is always the least recently touched. That is the entire LRU, with no new structure (parsimony\n // rungs 2/5 — the ordering we need is already a language guarantee).\n sessions.delete(agentId);\n sessions.set(agentId, existing);\n enforceCeiling();\n}\n\n/**\n * Ceiling on sessions kept in memory.\n *\n * The runtime only reads the **active** session; the rest is pure cache, rebuildable from the\n * on-disk transcript. 32 is deliberately generous — the primary removal path is the explicit\n * `discardSession()` at the end of the agent's life, and this ceiling is a safety net against a\n * long-lived process running hundreds of sessions (plan risk #2: a tight ceiling could evict a\n * session still referenced by an in-flight async path).\n */\nexport const MAX_CACHED_SESSIONS = 32;\n\nfunction enforceCeiling(): void {\n while (sessions.size > MAX_CACHED_SESSIONS) {\n const oldest = sessions.keys().next().value;\n if (oldest === undefined) return;\n sessions.delete(oldest);\n forgetBookkeeping(oldest);\n }\n}\n\n/**\n * Erases the bookkeeping for an `agentId` across EVERY `cwd` it appears in.\n *\n * The three maps are keyed by `transcriptKey(cwd, agentId)`; `sessions` is the only one keyed by the\n * raw `agentId`. The ceiling's first version deleted from all three using `sessions`' key — that is,\n * deleted nothing — and left `hydratedKeys` **orphaned**. Since `hydrateSession` returns early when\n * the marker is present, an evicted session came back **empty** instead of rehydrating from disk:\n * silent amnesia, and a regression new to M95, because before it nothing evicted.\n *\n * The ceiling only knows the `agentId`, not the `cwd`, so it scans by suffix — which is the format\n * `transcriptKey` produces. A scan rather than an index because these maps hold tens of entries, not\n * thousands: a reverse index here would be new structure for a problem that does not exist.\n */\nfunction forgetBookkeeping(agentId: string): void {\n const suffix = transcriptKey(\"\", agentId).slice(0 - agentId.length - 2);\n for (const k of [...hydratedKeys]) if (k.endsWith(suffix)) hydratedKeys.delete(k);\n for (const k of [...pendingWrites.keys()]) if (k.endsWith(suffix)) pendingWrites.delete(k);\n for (const k of [...recordCounts.keys()]) if (k.endsWith(suffix)) recordCounts.delete(k);\n}\n\n/**\n * Erases the agent's module bookkeeping and returns how many entries were removed.\n *\n * M95 — `invalidateSessionCache` used to clear **two** of the four maps (`sessions`, `hydratedKeys`);\n * `pendingWrites` and `recordCounts` were never touched by id, so they grew for the life of the\n * process. Neither is large per entry — the leak is in count, not volume — but a cache with no owner\n * for removal is a cache that only grows.\n *\n * Returns the count so the caller can prove the removal; a second discard returns 0, which is what\n * makes the idempotency test possible without exposing the maps.\n */\nexport function discardSession(cwd: string, agentId: string): number {\n const key = transcriptKey(cwd, agentId);\n let removed = 0;\n // `sessions` is NOT erased here — and the distinction is measured, not aesthetic. It holds the\n // readable conversation, and there is a legitimate reader AFTER dispose: the golden\n // `two-concurrent-sends-serialize` calls `getSessionMessages(agentId)` after `agent.dispose()`.\n // Erasing it here returned an empty list and broke two goldens. What bounds it is the LRU ceiling\n // above; the three below are pure bookkeeping, with no post-dispose reader.\n //\n // The key is `transcriptKey(cwd, agentId)` in ALL THREE — not the raw `agentId`. The first version\n // erased two of them by `agentId` and therefore **never erased anything**; the test did not catch\n // it because it only asserted that the SECOND call returns 0, which is true either way.\n if (hydratedKeys.delete(key)) removed++;\n if (pendingWrites.delete(key)) removed++;\n if (recordCounts.delete(key)) removed++;\n return removed;\n}\n\nexport function getSessionMessages(agentId: string): SessionMessage[] {\n return sessions.get(agentId) ?? [];\n}\n\n/**\n * Persist a full conversation turn (user + assistant + tool blocks) to the native\n * transcript. Chained per-(agent, transcript) so on-disk order matches send order,\n * and fire-and-forget so `send()` is not blocked by disk I/O. Every\n * M50 — when the caller supplies `turn.autoCompact`, size-driven auto-compaction (usage real vs\n * the model's context window) runs in this same chain, surfaced via the optional `onCompact` observer.\n *\n * @internal\n */\nexport function persistTurnToTranscript(\n store: SessionStore,\n loc: TranscriptLocation,\n sessionId: string,\n turn: PersistTurnInput,\n onCompact?: () => void,\n): void {\n const key = transcriptKey(loc.cwd, loc.agentId);\n // PRE-EXISTING debt, exposed when M75 fixed the Biome config that used to abort before\n // sweeping these files (a nested root under refactor/). It is not new code and was not touched\n // by M75; refactoring SDK internals without review would trade a visible problem for a diff\n // risky. Tracked in usetheodev/theokit-sdk#151.\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: see the reason just above\n const chained = (pendingWrites.get(key) ?? Promise.resolve()).then(async () => {\n try {\n await persistTurn(store, loc, sessionId, turn);\n const count = (recordCounts.get(key) ?? 0) + 1;\n recordCounts.set(key, count);\n // M50 — the 50-turn no-summary boundary stub is GONE (it silently amnesia'd the session).\n // Auto-compaction is now size-driven with a real summary: see `maybeAutoCompact` below,\n // invoked in this same write chain by the post-run lifecycle when usage is known.\n if (turn.autoCompact !== undefined) {\n const { autoCompactIfNeeded } = await import(\"./compact-session.js\");\n const fired = await autoCompactIfNeeded({\n store,\n loc,\n sessionId,\n usageTotal: turn.autoCompact.usageTotal,\n contextWindow: turn.autoCompact.contextWindow,\n turnCount: count,\n summarize: turn.autoCompact.summarize,\n });\n if (fired) onCompact?.();\n }\n } catch (cause) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n diag(`[theokit-sdk] session transcript write failed (${loc.agentId}): ${msg}\\n`);\n }\n });\n pendingWrites.set(\n key,\n chained.then(\n () => undefined,\n () => undefined,\n ),\n );\n}\n\n/**\n * Load the persisted transcript into the in-memory cache. Idempotent per\n * (baseDir, cwd, agentId). Call once per agent lifecycle before the first read.\n *\n * @internal\n */\nexport async function hydrateSession(\n agentId: string,\n loc: { store: SessionStore; cwd: string },\n): Promise<void> {\n const key = transcriptKey(loc.cwd, agentId);\n if (hydratedKeys.has(key)) return;\n hydratedKeys.add(key);\n\n const persisted = await readSessionMessages(loc.store, agentId);\n if (persisted.length === 0) return;\n // M51 review F4 — the DISK is the source of truth at hydration time: after an invalidation\n // (compact/inject), an in-flight turn may have repopulated the cache with a SINGLE message before\n // this hydrate ran; the old \"skip when non-empty\" guard then pinned the parent to a 1-message\n // context (history + injected pair lost until restart). The persist chain serializes writes, so\n // the disk already contains that in-flight turn — replacing is always correct.\n sessions.set(agentId, persisted);\n}\n\n/**\n * Wait for all pending transcript writes to settle. Used by tests and by the\n * agent dispose path so on-disk state matches in-memory before the caller proceeds.\n *\n * @internal\n */\nexport async function flushSessionWrites(): Promise<void> {\n while (pendingWrites.size > 0) {\n const all = Array.from(pendingWrites.values());\n pendingWrites.clear();\n await Promise.all(all);\n }\n}\n\n/**\n * Drop the in-memory message cache for `agentId` and NOTHING else — the hydration marker in\n * `hydratedKeys` deliberately stays.\n *\n * That asymmetry is the feature, not an oversight, and it is what separates this from\n * `invalidateSessionCache`, which drops both. The single caller is the personality switch's\n * `reset: true` path (`internal/personality/switch.ts`): leaving the marker set makes\n * `hydrateSession` return early, so the next send starts from an EMPTY context instead of\n * replaying the pre-reset transcript off disk. Clearing the marker here would re-hydrate the\n * conversation the reset was asked to discard.\n *\n * The same shape read as a bug one function up: `forgetBookkeeping` documents an evicted session\n * coming back empty as \"silent amnesia\". The difference is intent — eviction wants the transcript\n * back, a reset does not. Anyone reaching for this to invalidate a cache wants\n * `invalidateSessionCache(cwd, agentId)` instead.\n */\nexport function clearSession(agentId: string): void {\n sessions.delete(agentId);\n}\n\n/**\n * M50 review F5 — run `fn` serialized on the SAME per-(cwd,agentId) write chain the per-turn\n * persistence uses, so a manual `Agent.compact` can never interleave with an in-flight turn's\n * writes (boundary landing mid-turn would orphan the turn from the replay).\n */\nexport function enqueueSessionWrite<T>(\n cwd: string,\n agentId: string,\n fn: () => Promise<T>,\n): Promise<T> {\n const key = transcriptKey(cwd, agentId);\n const prior = pendingWrites.get(key) ?? Promise.resolve();\n const result = prior.then(fn);\n pendingWrites.set(\n key,\n result.then(\n () => undefined,\n () => undefined,\n ),\n );\n return result;\n}\n\n/** Test-only: drop every cached session and hydration marker. @internal */\nexport function clearAllSessions(): void {\n sessions.clear();\n hydratedKeys.clear();\n recordCounts.clear();\n // M50 review F11 — the auto-compact attempt marks live on globalThis; tests reset them here.\n // Through the helper, with the SAME key compact-session.ts uses: two files hand-rolling one slot is\n // how two copies of a mechanism drift apart.\n globalSingleton(\n \"theokit-sdk.session.auto-compact-attempts\",\n () => new Map<string, number>(),\n ).clear();\n}\n"]}
1
+ {"version":3,"sources":["../src/internal/session/agent-session.ts"],"names":[],"mappings":";;;;;AAyCA,IAAM,aAAA,uBAAoB,GAAA,EAA2B;AACrD,IAAM,YAAA,uBAAmB,GAAA,EAAoB;AAStC,SAAS,oBAAA,CAAqB,SAAiB,OAAA,EAA+B;AACnF,EAAA,MAAM,QAAA,GAAW,QAAA,CAAS,GAAA,CAAI,OAAO,KAAK,EAAC;AAC3C,EAAA,QAAA,CAAS,KAAK,OAAO,CAAA;AAIrB,EAAA,QAAA,CAAS,OAAO,OAAO,CAAA;AACvB,EAAA,QAAA,CAAS,GAAA,CAAI,SAAS,QAAQ,CAAA;AAC9B,EAAA,cAAA,EAAe;AACjB;AAWO,IAAM,mBAAA,GAAsB,EAAA;AAEnC,SAAS,cAAA,GAAuB;AAC9B,EAAA,OAAO,QAAA,CAAS,OAAO,mBAAA,EAAqB;AAC1C,IAAA,MAAM,MAAA,GAAS,QAAA,CAAS,IAAA,EAAK,CAAE,MAAK,CAAE,KAAA;AACtC,IAAA,IAAI,WAAW,MAAA,EAAW;AAC1B,IAAA,QAAA,CAAS,OAAO,MAAM,CAAA;AACtB,IAAA,iBAAA,CAAkB,MAAM,CAAA;AAAA,EAC1B;AACF;AAeA,SAAS,kBAAkB,OAAA,EAAuB;AAChD,EAAA,MAAM,MAAA,GAAS,cAAc,EAAA,EAAI,OAAO,EAAE,KAAA,CAAM,CAAA,GAAI,OAAA,CAAQ,MAAA,GAAS,CAAC,CAAA;AACtE,EAAA,KAAA,MAAW,CAAA,IAAK,CAAC,GAAG,YAAY,CAAA,EAAG,IAAI,CAAA,CAAE,QAAA,CAAS,MAAM,CAAA,EAAG,YAAA,CAAa,MAAA,CAAO,CAAC,CAAA;AAChF,EAAA,KAAA,MAAW,CAAA,IAAK,CAAC,GAAG,aAAA,CAAc,MAAM,CAAA,EAAG,IAAI,CAAA,CAAE,QAAA,CAAS,MAAM,CAAA,EAAG,aAAA,CAAc,OAAO,CAAC,CAAA;AACzF,EAAA,KAAA,MAAW,CAAA,IAAK,CAAC,GAAG,YAAA,CAAa,MAAM,CAAA,EAAG,IAAI,CAAA,CAAE,QAAA,CAAS,MAAM,CAAA,EAAG,YAAA,CAAa,OAAO,CAAC,CAAA;AACzF;AAaO,SAAS,cAAA,CAAe,KAAa,OAAA,EAAyB;AACnE,EAAA,MAAM,GAAA,GAAM,aAAA,CAAc,GAAA,EAAK,OAAO,CAAA;AACtC,EAAA,IAAI,OAAA,GAAU,CAAA;AAUd,EAAA,IAAI,YAAA,CAAa,MAAA,CAAO,GAAG,CAAA,EAAG,OAAA,EAAA;AAC9B,EAAA,IAAI,aAAA,CAAc,MAAA,CAAO,GAAG,CAAA,EAAG,OAAA,EAAA;AAC/B,EAAA,IAAI,YAAA,CAAa,MAAA,CAAO,GAAG,CAAA,EAAG,OAAA,EAAA;AAC9B,EAAA,OAAO,OAAA;AACT;AAEO,SAAS,mBAAmB,OAAA,EAAmC;AACpE,EAAA,OAAO,QAAA,CAAS,GAAA,CAAI,OAAO,CAAA,IAAK,EAAC;AACnC;AAWO,SAAS,uBAAA,CACd,KAAA,EACA,GAAA,EACA,SAAA,EACA,MACA,SAAA,EACM;AACN,EAAA,MAAM,GAAA,GAAM,aAAA,CAAc,GAAA,CAAI,GAAA,EAAK,IAAI,OAAO,CAAA;AAM9C,EAAA,MAAM,OAAA,GAAA,CAAW,cAAc,GAAA,CAAI,GAAG,KAAK,OAAA,CAAQ,OAAA,EAAQ,EAAG,IAAA,CAAK,YAAY;AAC7E,IAAA,IAAI;AACF,MAAA,MAAM,WAAA,CAAY,KAAA,EAAO,GAAA,EAAK,SAAA,EAAW,IAAI,CAAA;AAC7C,MAAA,MAAM,KAAA,GAAA,CAAS,YAAA,CAAa,GAAA,CAAI,GAAG,KAAK,CAAA,IAAK,CAAA;AAC7C,MAAA,YAAA,CAAa,GAAA,CAAI,KAAK,KAAK,CAAA;AAI3B,MAAA,IAAI,IAAA,CAAK,gBAAgB,KAAA,CAAA,EAAW;AAClC,QAAA,MAAM,EAAE,mBAAA,EAAoB,GAAI,MAAM,OAAO,+BAAsB,CAAA;AACnE,QAAA,MAAM,KAAA,GAAQ,MAAM,mBAAA,CAAoB;AAAA,UACtC,KAAA;AAAA,UACA,GAAA;AAAA,UACA,SAAA;AAAA,UACA,UAAA,EAAY,KAAK,WAAA,CAAY,UAAA;AAAA,UAC7B,aAAA,EAAe,KAAK,WAAA,CAAY,aAAA;AAAA,UAChC,SAAA,EAAW,KAAA;AAAA,UACX,SAAA,EAAW,KAAK,WAAA,CAAY;AAAA,SAC7B,CAAA;AACD,QAAA,IAAI,OAAO,SAAA,IAAY;AAAA,MACzB;AAAA,IACF,SAAS,KAAA,EAAO;AACd,MAAA,MAAM,MAAM,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,OAAO,KAAK,CAAA;AACjE,MAAA,IAAA,CAAK,CAAA,+CAAA,EAAkD,GAAA,CAAI,OAAO,CAAA,GAAA,EAAM,GAAG;AAAA,CAAI,CAAA;AAAA,IACjF;AAAA,EACF,CAAC,CAAA;AACD,EAAA,aAAA,CAAc,GAAA;AAAA,IACZ,GAAA;AAAA,IACA,OAAA,CAAQ,IAAA;AAAA,MACN,MAAM,MAAA;AAAA,MACN,MAAM;AAAA;AACR,GACF;AACF;AAQA,eAAsB,cAAA,CACpB,SACA,GAAA,EACe;AACf,EAAA,MAAM,GAAA,GAAM,aAAA,CAAc,GAAA,CAAI,GAAA,EAAK,OAAO,CAAA;AAC1C,EAAA,IAAI,YAAA,CAAa,GAAA,CAAI,GAAG,CAAA,EAAG;AAC3B,EAAA,YAAA,CAAa,IAAI,GAAG,CAAA;AAEpB,EAAA,MAAM,SAAA,GAAY,MAAM,mBAAA,CAAoB,GAAA,CAAI,OAAO,OAAO,CAAA;AAC9D,EAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AAM5B,EAAA,QAAA,CAAS,GAAA,CAAI,SAAS,SAAS,CAAA;AACjC;AAQA,eAAsB,kBAAA,GAAoC;AACxD,EAAA,OAAO,aAAA,CAAc,OAAO,CAAA,EAAG;AAC7B,IAAA,MAAM,GAAA,GAAM,KAAA,CAAM,IAAA,CAAK,aAAA,CAAc,QAAQ,CAAA;AAC7C,IAAA,aAAA,CAAc,KAAA,EAAM;AACpB,IAAA,MAAM,OAAA,CAAQ,IAAI,GAAG,CAAA;AAAA,EACvB;AACF;AAkBO,SAAS,aAAa,OAAA,EAAuB;AAClD,EAAA,QAAA,CAAS,OAAO,OAAO,CAAA;AACzB;AAOO,SAAS,mBAAA,CACd,GAAA,EACA,OAAA,EACA,EAAA,EACY;AACZ,EAAA,MAAM,GAAA,GAAM,aAAA,CAAc,GAAA,EAAK,OAAO,CAAA;AACtC,EAAA,MAAM,QAAQ,aAAA,CAAc,GAAA,CAAI,GAAG,CAAA,IAAK,QAAQ,OAAA,EAAQ;AACxD,EAAA,MAAM,MAAA,GAAS,KAAA,CAAM,IAAA,CAAK,EAAE,CAAA;AAC5B,EAAA,aAAA,CAAc,GAAA;AAAA,IACZ,GAAA;AAAA,IACA,MAAA,CAAO,IAAA;AAAA,MACL,MAAM,MAAA;AAAA,MACN,MAAM;AAAA;AACR,GACF;AACA,EAAA,OAAO,MAAA;AACT","file":"chunk-2QKV4CGK.js","sourcesContent":["import type { SessionStore } from \"../../types/session-store.js\";\nimport {\n type PersistTurnInput,\n persistTurn,\n readSessionMessages,\n type TranscriptLocation,\n} from \"./agent-session-store.js\";\n\n/**\n * Per-agent conversation history kept across runs (and across `Agent.resume()`\n * within the same process). Lets the fixture responder recall prior facts when\n * the user asks a follow-up question.\n *\n * SE40 — disk persistence IS the native Claude-shaped `.jsonl` transcript. The\n * in-memory cache holds the narrowed `SessionMessage[]` (user/assistant text) so\n * a send can read `priorMessages` synchronously; the whole rich turn (user +\n * assistant + tool blocks) lands on disk once per send via {@link persistTurnToTranscript}\n * (fed by `run.conversation()`). Hydration on resume reconstructs the session by\n * walking the transcript DAG (`readSessionMessages`).\n *\n * @internal\n */\n\n// `SessionMessage` lives in `./session-types.ts` (leaf types file). Re-exported\n// for back-compat with downstream importers that pulled it from here.\nexport type { SessionMessage } from \"./types.js\";\n\nimport type { SessionMessage } from \"./types.js\";\n\n// M75 — the cache lives in a leaf; see session-cache.ts for the reason (cycle broken by extraction).\nexport {\n hydratedKeys,\n invalidateSessionCache,\n sessions,\n transcriptKey,\n} from \"./session-cache.js\";\n\nimport { diag } from \"../diagnostics.js\";\nimport { globalSingleton } from \"../global-singleton.js\";\nimport { hydratedKeys, sessions, transcriptKey } from \"./session-cache.js\";\n\nconst pendingWrites = new Map<string, Promise<void>>();\nconst recordCounts = new Map<string, number>();\n\n/**\n * Append a session message to the in-memory cache only. Disk persistence for the\n * whole turn happens once per send via {@link persistTurnToTranscript}; the cache\n * feeds `priorMessages` / `onBeforeSend.previousMessageCount` synchronously.\n *\n * @internal\n */\nexport function appendSessionMessage(agentId: string, message: SessionMessage): void {\n const existing = sessions.get(agentId) ?? [];\n existing.push(message);\n // `delete` + `set` reinserts at the END: a JS `Map` preserves insertion order, so the first key\n // is always the least recently touched. That is the entire LRU, with no new structure (parsimony\n // rungs 2/5 — the ordering we need is already a language guarantee).\n sessions.delete(agentId);\n sessions.set(agentId, existing);\n enforceCeiling();\n}\n\n/**\n * Ceiling on sessions kept in memory.\n *\n * The runtime only reads the **active** session; the rest is pure cache, rebuildable from the\n * on-disk transcript. 32 is deliberately generous — the primary removal path is the explicit\n * `discardSession()` at the end of the agent's life, and this ceiling is a safety net against a\n * long-lived process running hundreds of sessions (plan risk #2: a tight ceiling could evict a\n * session still referenced by an in-flight async path).\n */\nexport const MAX_CACHED_SESSIONS = 32;\n\nfunction enforceCeiling(): void {\n while (sessions.size > MAX_CACHED_SESSIONS) {\n const oldest = sessions.keys().next().value;\n if (oldest === undefined) return;\n sessions.delete(oldest);\n forgetBookkeeping(oldest);\n }\n}\n\n/**\n * Erases the bookkeeping for an `agentId` across EVERY `cwd` it appears in.\n *\n * The three maps are keyed by `transcriptKey(cwd, agentId)`; `sessions` is the only one keyed by the\n * raw `agentId`. The ceiling's first version deleted from all three using `sessions`' key — that is,\n * deleted nothing — and left `hydratedKeys` **orphaned**. Since `hydrateSession` returns early when\n * the marker is present, an evicted session came back **empty** instead of rehydrating from disk:\n * silent amnesia, and a regression new to M95, because before it nothing evicted.\n *\n * The ceiling only knows the `agentId`, not the `cwd`, so it scans by suffix — which is the format\n * `transcriptKey` produces. A scan rather than an index because these maps hold tens of entries, not\n * thousands: a reverse index here would be new structure for a problem that does not exist.\n */\nfunction forgetBookkeeping(agentId: string): void {\n const suffix = transcriptKey(\"\", agentId).slice(0 - agentId.length - 2);\n for (const k of [...hydratedKeys]) if (k.endsWith(suffix)) hydratedKeys.delete(k);\n for (const k of [...pendingWrites.keys()]) if (k.endsWith(suffix)) pendingWrites.delete(k);\n for (const k of [...recordCounts.keys()]) if (k.endsWith(suffix)) recordCounts.delete(k);\n}\n\n/**\n * Erases the agent's module bookkeeping and returns how many entries were removed.\n *\n * M95 — `invalidateSessionCache` used to clear **two** of the four maps (`sessions`, `hydratedKeys`);\n * `pendingWrites` and `recordCounts` were never touched by id, so they grew for the life of the\n * process. Neither is large per entry — the leak is in count, not volume — but a cache with no owner\n * for removal is a cache that only grows.\n *\n * Returns the count so the caller can prove the removal; a second discard returns 0, which is what\n * makes the idempotency test possible without exposing the maps.\n */\nexport function discardSession(cwd: string, agentId: string): number {\n const key = transcriptKey(cwd, agentId);\n let removed = 0;\n // `sessions` is NOT erased here — and the distinction is measured, not aesthetic. It holds the\n // readable conversation, and there is a legitimate reader AFTER dispose: the golden\n // `two-concurrent-sends-serialize` calls `getSessionMessages(agentId)` after `agent.dispose()`.\n // Erasing it here returned an empty list and broke two goldens. What bounds it is the LRU ceiling\n // above; the three below are pure bookkeeping, with no post-dispose reader.\n //\n // The key is `transcriptKey(cwd, agentId)` in ALL THREE — not the raw `agentId`. The first version\n // erased two of them by `agentId` and therefore **never erased anything**; the test did not catch\n // it because it only asserted that the SECOND call returns 0, which is true either way.\n if (hydratedKeys.delete(key)) removed++;\n if (pendingWrites.delete(key)) removed++;\n if (recordCounts.delete(key)) removed++;\n return removed;\n}\n\nexport function getSessionMessages(agentId: string): SessionMessage[] {\n return sessions.get(agentId) ?? [];\n}\n\n/**\n * Persist a full conversation turn (user + assistant + tool blocks) to the native\n * transcript. Chained per-(agent, transcript) so on-disk order matches send order,\n * and fire-and-forget so `send()` is not blocked by disk I/O. Every\n * M50 — when the caller supplies `turn.autoCompact`, size-driven auto-compaction (usage real vs\n * the model's context window) runs in this same chain, surfaced via the optional `onCompact` observer.\n *\n * @internal\n */\nexport function persistTurnToTranscript(\n store: SessionStore,\n loc: TranscriptLocation,\n sessionId: string,\n turn: PersistTurnInput,\n onCompact?: () => void,\n): void {\n const key = transcriptKey(loc.cwd, loc.agentId);\n // PRE-EXISTING debt, exposed when M75 fixed the Biome config that used to abort before\n // sweeping these files (a nested root under refactor/). It is not new code and was not touched\n // by M75; refactoring SDK internals without review would trade a visible problem for a diff\n // risky. Tracked in usetheodev/theokit-sdk#151.\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: see the reason just above\n const chained = (pendingWrites.get(key) ?? Promise.resolve()).then(async () => {\n try {\n await persistTurn(store, loc, sessionId, turn);\n const count = (recordCounts.get(key) ?? 0) + 1;\n recordCounts.set(key, count);\n // M50 — the 50-turn no-summary boundary stub is GONE (it silently amnesia'd the session).\n // Auto-compaction is now size-driven with a real summary: see `maybeAutoCompact` below,\n // invoked in this same write chain by the post-run lifecycle when usage is known.\n if (turn.autoCompact !== undefined) {\n const { autoCompactIfNeeded } = await import(\"./compact-session.js\");\n const fired = await autoCompactIfNeeded({\n store,\n loc,\n sessionId,\n usageTotal: turn.autoCompact.usageTotal,\n contextWindow: turn.autoCompact.contextWindow,\n turnCount: count,\n summarize: turn.autoCompact.summarize,\n });\n if (fired) onCompact?.();\n }\n } catch (cause) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n diag(`[theokit-sdk] session transcript write failed (${loc.agentId}): ${msg}\\n`);\n }\n });\n pendingWrites.set(\n key,\n chained.then(\n () => undefined,\n () => undefined,\n ),\n );\n}\n\n/**\n * Load the persisted transcript into the in-memory cache. Idempotent per\n * (baseDir, cwd, agentId). Call once per agent lifecycle before the first read.\n *\n * @internal\n */\nexport async function hydrateSession(\n agentId: string,\n loc: { store: SessionStore; cwd: string },\n): Promise<void> {\n const key = transcriptKey(loc.cwd, agentId);\n if (hydratedKeys.has(key)) return;\n hydratedKeys.add(key);\n\n const persisted = await readSessionMessages(loc.store, agentId);\n if (persisted.length === 0) return;\n // M51 review F4 — the DISK is the source of truth at hydration time: after an invalidation\n // (compact/inject), an in-flight turn may have repopulated the cache with a SINGLE message before\n // this hydrate ran; the old \"skip when non-empty\" guard then pinned the parent to a 1-message\n // context (history + injected pair lost until restart). The persist chain serializes writes, so\n // the disk already contains that in-flight turn — replacing is always correct.\n sessions.set(agentId, persisted);\n}\n\n/**\n * Wait for all pending transcript writes to settle. Used by tests and by the\n * agent dispose path so on-disk state matches in-memory before the caller proceeds.\n *\n * @internal\n */\nexport async function flushSessionWrites(): Promise<void> {\n while (pendingWrites.size > 0) {\n const all = Array.from(pendingWrites.values());\n pendingWrites.clear();\n await Promise.all(all);\n }\n}\n\n/**\n * Drop the in-memory message cache for `agentId` and NOTHING else — the hydration marker in\n * `hydratedKeys` deliberately stays.\n *\n * That asymmetry is the feature, not an oversight, and it is what separates this from\n * `invalidateSessionCache`, which drops both. The single caller is the personality switch's\n * `reset: true` path (`internal/personality/switch.ts`): leaving the marker set makes\n * `hydrateSession` return early, so the next send starts from an EMPTY context instead of\n * replaying the pre-reset transcript off disk. Clearing the marker here would re-hydrate the\n * conversation the reset was asked to discard.\n *\n * The same shape read as a bug one function up: `forgetBookkeeping` documents an evicted session\n * coming back empty as \"silent amnesia\". The difference is intent — eviction wants the transcript\n * back, a reset does not. Anyone reaching for this to invalidate a cache wants\n * `invalidateSessionCache(cwd, agentId)` instead.\n */\nexport function clearSession(agentId: string): void {\n sessions.delete(agentId);\n}\n\n/**\n * M50 review F5 — run `fn` serialized on the SAME per-(cwd,agentId) write chain the per-turn\n * persistence uses, so a manual `Agent.compact` can never interleave with an in-flight turn's\n * writes (boundary landing mid-turn would orphan the turn from the replay).\n */\nexport function enqueueSessionWrite<T>(\n cwd: string,\n agentId: string,\n fn: () => Promise<T>,\n): Promise<T> {\n const key = transcriptKey(cwd, agentId);\n const prior = pendingWrites.get(key) ?? Promise.resolve();\n const result = prior.then(fn);\n pendingWrites.set(\n key,\n result.then(\n () => undefined,\n () => undefined,\n ),\n );\n return result;\n}\n\n/** Test-only: drop every cached session and hydration marker. @internal */\nexport function clearAllSessions(): void {\n sessions.clear();\n hydratedKeys.clear();\n recordCounts.clear();\n // M50 review F11 — the auto-compact attempt marks live on globalThis; tests reset them here.\n // Through the helper, with the SAME key compact-session.ts uses: two files hand-rolling one slot is\n // how two copies of a mechanism drift apart.\n globalSingleton(\n \"theokit-sdk.session.auto-compact-attempts\",\n () => new Map<string, number>(),\n ).clear();\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkY46VI73J_cjs = require('./chunk-Y46VI73J.cjs');
3
+ var chunkUKJJRU7C_cjs = require('./chunk-UKJJRU7C.cjs');
4
4
  var chunkKVSAY6NZ_cjs = require('./chunk-KVSAY6NZ.cjs');
5
5
  var chunkNINIJCHU_cjs = require('./chunk-NINIJCHU.cjs');
6
6
  var chunkJ7J7J2GN_cjs = require('./chunk-J7J7J2GN.cjs');
@@ -376,7 +376,7 @@ async function createCronJob(options) {
376
376
  const now = Date.now();
377
377
  const nextFireAt = nextRunAt(options.cron, timezone);
378
378
  const job = {
379
- id: chunkY46VI73J_cjs.generateCronId(),
379
+ id: chunkUKJJRU7C_cjs.generateCronId(),
380
380
  cron: options.cron,
381
381
  timezone,
382
382
  enabled: options.enabled ?? true,
@@ -453,5 +453,5 @@ async function updateJobStatus(jobId, enabled) {
453
453
  }
454
454
 
455
455
  exports.Cron = Cron2;
456
- //# sourceMappingURL=chunk-L4XENLQO.cjs.map
457
- //# sourceMappingURL=chunk-L4XENLQO.cjs.map
456
+ //# sourceMappingURL=chunk-2ZLQZHZC.cjs.map
457
+ //# sourceMappingURL=chunk-2ZLQZHZC.cjs.map