@pooder/core 1.2.0 → 2.1.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.
package/dist/index.mjs CHANGED
@@ -245,6 +245,11 @@ var ExtensionManager = class {
245
245
  const commandService = this.context.services.get("CommandService");
246
246
  return commandService.registerCommand(item.id, item.data.handler);
247
247
  }
248
+ if (pointId === ContributionPointIds.TOOLS) {
249
+ const toolRegistry = this.context.services.get("ToolRegistryService");
250
+ if (!toolRegistry) return;
251
+ return toolRegistry.registerTool(item.data);
252
+ }
248
253
  }
249
254
  unregister(name) {
250
255
  const extension = this.extensionRegistry.get(name);
@@ -447,6 +452,289 @@ var ConfigurationService = class {
447
452
  }
448
453
  };
449
454
 
455
+ // src/services/ToolRegistryService.ts
456
+ var ToolRegistryService = class {
457
+ constructor() {
458
+ this.tools = /* @__PURE__ */ new Map();
459
+ }
460
+ registerTool(tool) {
461
+ if (!(tool == null ? void 0 : tool.id)) {
462
+ throw new Error("ToolContribution.id is required.");
463
+ }
464
+ this.tools.set(tool.id, tool);
465
+ return {
466
+ dispose: () => {
467
+ if (this.tools.get(tool.id) === tool) {
468
+ this.tools.delete(tool.id);
469
+ }
470
+ }
471
+ };
472
+ }
473
+ unregisterTool(toolId) {
474
+ this.tools.delete(toolId);
475
+ }
476
+ getTool(toolId) {
477
+ return this.tools.get(toolId);
478
+ }
479
+ listTools() {
480
+ return Array.from(this.tools.values());
481
+ }
482
+ hasTool(toolId) {
483
+ return this.tools.has(toolId);
484
+ }
485
+ dispose() {
486
+ this.tools.clear();
487
+ }
488
+ };
489
+
490
+ // src/services/ToolSessionService.ts
491
+ var ToolSessionService = class {
492
+ constructor() {
493
+ this.sessions = /* @__PURE__ */ new Map();
494
+ this.dirtyTrackers = /* @__PURE__ */ new Map();
495
+ }
496
+ setCommandService(commandService) {
497
+ this.commandService = commandService;
498
+ }
499
+ setToolRegistry(toolRegistry) {
500
+ this.toolRegistry = toolRegistry;
501
+ }
502
+ registerDirtyTracker(toolId, callback) {
503
+ const wrapped = () => {
504
+ try {
505
+ return callback();
506
+ } catch (e) {
507
+ return false;
508
+ }
509
+ };
510
+ this.dirtyTrackers.set(toolId, wrapped);
511
+ return {
512
+ dispose: () => {
513
+ if (this.dirtyTrackers.get(toolId) === wrapped) {
514
+ this.dirtyTrackers.delete(toolId);
515
+ }
516
+ }
517
+ };
518
+ }
519
+ ensureSession(toolId) {
520
+ const existing = this.sessions.get(toolId);
521
+ if (existing) return existing;
522
+ const created = {
523
+ toolId,
524
+ status: "idle",
525
+ dirty: false
526
+ };
527
+ this.sessions.set(toolId, created);
528
+ return created;
529
+ }
530
+ getState(toolId) {
531
+ return { ...this.ensureSession(toolId) };
532
+ }
533
+ isDirty(toolId) {
534
+ const tracker = this.dirtyTrackers.get(toolId);
535
+ if (tracker) return tracker();
536
+ return this.ensureSession(toolId).dirty;
537
+ }
538
+ markDirty(toolId, dirty = true) {
539
+ const session = this.ensureSession(toolId);
540
+ session.dirty = dirty;
541
+ session.lastUpdatedAt = Date.now();
542
+ }
543
+ resolveTool(toolId) {
544
+ var _a;
545
+ return (_a = this.toolRegistry) == null ? void 0 : _a.getTool(toolId);
546
+ }
547
+ async runCommand(commandId, ...args) {
548
+ if (!commandId || !this.commandService) return void 0;
549
+ return await this.commandService.executeCommand(commandId, ...args);
550
+ }
551
+ async begin(toolId) {
552
+ var _a;
553
+ const tool = this.resolveTool(toolId);
554
+ const session = this.ensureSession(toolId);
555
+ if (session.status === "active") return;
556
+ await this.runCommand((_a = tool == null ? void 0 : tool.commands) == null ? void 0 : _a.begin);
557
+ session.status = "active";
558
+ session.startedAt = Date.now();
559
+ session.lastUpdatedAt = session.startedAt;
560
+ }
561
+ async validate(toolId) {
562
+ var _a;
563
+ const tool = this.resolveTool(toolId);
564
+ if (!((_a = tool == null ? void 0 : tool.commands) == null ? void 0 : _a.validate)) {
565
+ return { ok: true };
566
+ }
567
+ const result = await this.runCommand(tool.commands.validate);
568
+ if (result === false) return { ok: false, result };
569
+ if (result && typeof result === "object" && "ok" in result) {
570
+ return { ok: Boolean(result.ok), result };
571
+ }
572
+ return { ok: true, result };
573
+ }
574
+ async commit(toolId) {
575
+ var _a;
576
+ const tool = this.resolveTool(toolId);
577
+ const validateResult = await this.validate(toolId);
578
+ if (!validateResult.ok) return validateResult;
579
+ const result = await this.runCommand((_a = tool == null ? void 0 : tool.commands) == null ? void 0 : _a.commit);
580
+ const session = this.ensureSession(toolId);
581
+ session.dirty = false;
582
+ session.status = "idle";
583
+ session.lastUpdatedAt = Date.now();
584
+ return { ok: true, result };
585
+ }
586
+ async rollback(toolId) {
587
+ var _a, _b;
588
+ const tool = this.resolveTool(toolId);
589
+ await this.runCommand(((_a = tool == null ? void 0 : tool.commands) == null ? void 0 : _a.rollback) || ((_b = tool == null ? void 0 : tool.commands) == null ? void 0 : _b.reset));
590
+ const session = this.ensureSession(toolId);
591
+ session.dirty = false;
592
+ session.status = "idle";
593
+ session.lastUpdatedAt = Date.now();
594
+ }
595
+ deactivateSession(toolId) {
596
+ const session = this.ensureSession(toolId);
597
+ session.status = "idle";
598
+ session.lastUpdatedAt = Date.now();
599
+ }
600
+ async handleBeforeLeave(toolId) {
601
+ var _a, _b;
602
+ const tool = this.resolveTool(toolId);
603
+ if (!tool) return { decision: "allow" };
604
+ if (tool.interaction !== "session") return { decision: "allow" };
605
+ const dirty = this.isDirty(toolId);
606
+ if (!dirty) return { decision: "allow" };
607
+ const leavePolicy = (_b = (_a = tool.session) == null ? void 0 : _a.leavePolicy) != null ? _b : "block";
608
+ if (leavePolicy === "commit") {
609
+ const committed = await this.commit(toolId);
610
+ if (!committed.ok) {
611
+ return { decision: "blocked", reason: "session-validation-failed" };
612
+ }
613
+ return { decision: "allow" };
614
+ }
615
+ if (leavePolicy === "rollback") {
616
+ await this.rollback(toolId);
617
+ return { decision: "allow" };
618
+ }
619
+ return { decision: "blocked", reason: "session-dirty" };
620
+ }
621
+ dispose() {
622
+ this.sessions.clear();
623
+ this.dirtyTrackers.clear();
624
+ }
625
+ };
626
+
627
+ // src/services/WorkbenchService.ts
628
+ var WorkbenchService = class {
629
+ constructor() {
630
+ this._activeToolId = null;
631
+ this.guards = [];
632
+ }
633
+ init() {
634
+ }
635
+ dispose() {
636
+ this.guards = [];
637
+ }
638
+ setEventBus(bus) {
639
+ this.eventBus = bus;
640
+ }
641
+ setToolRegistry(toolRegistry) {
642
+ this.toolRegistry = toolRegistry;
643
+ }
644
+ setToolSessionService(sessionService) {
645
+ this.sessionService = sessionService;
646
+ }
647
+ get activeToolId() {
648
+ return this._activeToolId;
649
+ }
650
+ registerSwitchGuard(guard, priority = 0) {
651
+ const item = { guard, priority };
652
+ this.guards.push(item);
653
+ this.guards.sort((a, b) => b.priority - a.priority);
654
+ return {
655
+ dispose: () => {
656
+ const index = this.guards.indexOf(item);
657
+ if (index >= 0) this.guards.splice(index, 1);
658
+ }
659
+ };
660
+ }
661
+ async runGuards(context) {
662
+ for (const { guard } of this.guards) {
663
+ const allowed = await Promise.resolve(guard(context));
664
+ if (!allowed) return false;
665
+ }
666
+ return true;
667
+ }
668
+ async switchTool(id, options) {
669
+ var _a, _b, _c, _d, _e;
670
+ if (this._activeToolId === id) {
671
+ return { ok: true, from: this._activeToolId, to: id };
672
+ }
673
+ if (id && this.toolRegistry && !this.toolRegistry.hasTool(id)) {
674
+ return {
675
+ ok: false,
676
+ from: this._activeToolId,
677
+ to: id,
678
+ reason: `tool-not-registered:${id}`
679
+ };
680
+ }
681
+ const context = {
682
+ from: this._activeToolId,
683
+ to: id,
684
+ reason: options == null ? void 0 : options.reason
685
+ };
686
+ const guardAllowed = await this.runGuards(context);
687
+ if (!guardAllowed) {
688
+ (_a = this.eventBus) == null ? void 0 : _a.emit("tool:switch:blocked", {
689
+ ...context,
690
+ reason: "blocked-by-guard"
691
+ });
692
+ return {
693
+ ok: false,
694
+ from: this._activeToolId,
695
+ to: id,
696
+ reason: "blocked-by-guard"
697
+ };
698
+ }
699
+ if (context.from && this.sessionService) {
700
+ const leaveResult = await this.sessionService.handleBeforeLeave(
701
+ context.from
702
+ );
703
+ if (leaveResult.decision === "blocked") {
704
+ (_b = this.eventBus) == null ? void 0 : _b.emit("tool:switch:blocked", {
705
+ ...context,
706
+ reason: leaveResult.reason || "session-blocked"
707
+ });
708
+ return {
709
+ ok: false,
710
+ from: this._activeToolId,
711
+ to: id,
712
+ reason: leaveResult.reason || "session-blocked"
713
+ };
714
+ }
715
+ this.sessionService.deactivateSession(context.from);
716
+ }
717
+ if (id && this.sessionService && this.toolRegistry) {
718
+ const tool = this.toolRegistry.getTool(id);
719
+ if ((tool == null ? void 0 : tool.interaction) === "session" && ((_c = tool.session) == null ? void 0 : _c.autoBegin) !== false) {
720
+ await this.sessionService.begin(id);
721
+ }
722
+ }
723
+ const previous = this._activeToolId;
724
+ this._activeToolId = id;
725
+ const reason = options == null ? void 0 : options.reason;
726
+ (_d = this.eventBus) == null ? void 0 : _d.emit("tool:activated", { id, previous, reason });
727
+ (_e = this.eventBus) == null ? void 0 : _e.emit("tool:switch", { from: previous, to: id, reason });
728
+ return { ok: true, from: previous, to: id };
729
+ }
730
+ async activate(id) {
731
+ return await this.switchTool(id, { reason: "activate" });
732
+ }
733
+ async deactivate() {
734
+ return await this.switchTool(null, { reason: "deactivate" });
735
+ }
736
+ };
737
+
450
738
  // src/index.ts
451
739
  var Pooder = class {
452
740
  constructor() {
@@ -458,6 +746,17 @@ var Pooder = class {
458
746
  this.registerService(commandService, "CommandService");
459
747
  const configurationService = new ConfigurationService();
460
748
  this.registerService(configurationService, "ConfigurationService");
749
+ const toolRegistryService = new ToolRegistryService();
750
+ this.registerService(toolRegistryService, "ToolRegistryService");
751
+ const toolSessionService = new ToolSessionService();
752
+ toolSessionService.setCommandService(commandService);
753
+ toolSessionService.setToolRegistry(toolRegistryService);
754
+ this.registerService(toolSessionService, "ToolSessionService");
755
+ const workbenchService = new WorkbenchService();
756
+ workbenchService.setEventBus(this.eventBus);
757
+ workbenchService.setToolRegistry(toolRegistryService);
758
+ workbenchService.setToolSessionService(toolSessionService);
759
+ this.registerService(workbenchService, "WorkbenchService");
461
760
  const context = {
462
761
  eventBus: this.eventBus,
463
762
  services: {
@@ -545,8 +844,12 @@ export {
545
844
  ConfigurationService,
546
845
  ContributionPointIds,
547
846
  ContributionRegistry,
847
+ event_default as EventBus,
548
848
  ExtensionManager,
549
849
  ExtensionRegistry,
550
850
  Pooder,
551
- ServiceRegistry
851
+ ServiceRegistry,
852
+ ToolRegistryService,
853
+ ToolSessionService,
854
+ WorkbenchService
552
855
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pooder/core",
3
- "version": "1.2.0",
3
+ "version": "2.1.0",
4
4
  "description": "Core logic for Pooder editor",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -17,12 +17,36 @@ export interface CommandContribution {
17
17
  /**
18
18
  * Tool Contribution Data Definition
19
19
  */
20
+ export type ToolInteraction = "instant" | "session" | "hybrid";
21
+
22
+ export type ToolSessionLeavePolicy = "block" | "commit" | "rollback";
23
+
24
+ export interface ToolCommandBindings {
25
+ execute?: string;
26
+ begin?: string;
27
+ validate?: string;
28
+ commit?: string;
29
+ rollback?: string;
30
+ reset?: string;
31
+ }
32
+
20
33
  export interface ToolContribution {
21
34
  id: string;
22
35
  name: string;
23
- description: string;
24
- parameters?: Record<string, any>; // JSON Schema for parameters
25
- execute: (...args: any[]) => Promise<any>;
36
+ description?: string;
37
+ icon?: string;
38
+ interaction: ToolInteraction;
39
+ parameters?: Record<string, any>;
40
+ commands?: ToolCommandBindings;
41
+ view?: {
42
+ id?: string;
43
+ type?: "sidebar" | "panel" | "editor" | "dialog";
44
+ location?: string;
45
+ };
46
+ session?: {
47
+ autoBegin?: boolean;
48
+ leavePolicy?: ToolSessionLeavePolicy;
49
+ };
26
50
  }
27
51
 
28
52
  /**
package/src/extension.ts CHANGED
@@ -3,6 +3,7 @@ import { Contribution, ContributionPointIds } from "./contribution";
3
3
  import Disposable from "./disposable";
4
4
  import CommandService from "./services/CommandService";
5
5
  import { ConfigurationService } from "./services";
6
+ import ToolRegistryService from "./services/ToolRegistryService";
6
7
 
7
8
  interface ExtensionMetadata {
8
9
  name: string;
@@ -110,6 +111,12 @@ class ExtensionManager {
110
111
 
111
112
  return commandService.registerCommand(item.id, item.data.handler);
112
113
  }
114
+ if (pointId === ContributionPointIds.TOOLS) {
115
+ const toolRegistry =
116
+ this.context.services.get<ToolRegistryService>("ToolRegistryService");
117
+ if (!toolRegistry) return;
118
+ return toolRegistry.registerTool(item.data);
119
+ }
113
120
  }
114
121
 
115
122
  unregister(name: string) {
package/src/index.ts CHANGED
@@ -8,7 +8,13 @@ import {
8
8
  ContributionPointIds,
9
9
  ContributionRegistry,
10
10
  } from "./contribution";
11
- import { CommandService, ConfigurationService } from "./services";
11
+ import {
12
+ CommandService,
13
+ ConfigurationService,
14
+ ToolRegistryService,
15
+ ToolSessionService,
16
+ WorkbenchService,
17
+ } from "./services";
12
18
  import { ExtensionContext } from "./context";
13
19
 
14
20
  export * from "./extension";
@@ -16,6 +22,7 @@ export * from "./context";
16
22
  export * from "./contribution";
17
23
  export * from "./service";
18
24
  export * from "./services";
25
+ export { default as EventBus } from "./event";
19
26
 
20
27
  export class Pooder {
21
28
  readonly eventBus: EventBus = new EventBus();
@@ -34,6 +41,20 @@ export class Pooder {
34
41
  const configurationService = new ConfigurationService();
35
42
  this.registerService(configurationService, "ConfigurationService");
36
43
 
44
+ const toolRegistryService = new ToolRegistryService();
45
+ this.registerService(toolRegistryService, "ToolRegistryService");
46
+
47
+ const toolSessionService = new ToolSessionService();
48
+ toolSessionService.setCommandService(commandService);
49
+ toolSessionService.setToolRegistry(toolRegistryService);
50
+ this.registerService(toolSessionService, "ToolSessionService");
51
+
52
+ const workbenchService = new WorkbenchService();
53
+ workbenchService.setEventBus(this.eventBus);
54
+ workbenchService.setToolRegistry(toolRegistryService);
55
+ workbenchService.setToolSessionService(toolSessionService);
56
+ this.registerService(workbenchService, "WorkbenchService");
57
+
37
58
  // Create a restricted context for extensions
38
59
  const context: ExtensionContext = {
39
60
  eventBus: this.eventBus,
@@ -0,0 +1,41 @@
1
+ import { ToolContribution } from "../contribution";
2
+ import Disposable from "../disposable";
3
+ import { Service } from "../service";
4
+
5
+ export default class ToolRegistryService implements Service {
6
+ private tools = new Map<string, ToolContribution>();
7
+
8
+ registerTool(tool: ToolContribution): Disposable {
9
+ if (!tool?.id) {
10
+ throw new Error("ToolContribution.id is required.");
11
+ }
12
+ this.tools.set(tool.id, tool);
13
+ return {
14
+ dispose: () => {
15
+ if (this.tools.get(tool.id) === tool) {
16
+ this.tools.delete(tool.id);
17
+ }
18
+ },
19
+ };
20
+ }
21
+
22
+ unregisterTool(toolId: string) {
23
+ this.tools.delete(toolId);
24
+ }
25
+
26
+ getTool(toolId: string): ToolContribution | undefined {
27
+ return this.tools.get(toolId);
28
+ }
29
+
30
+ listTools(): ToolContribution[] {
31
+ return Array.from(this.tools.values());
32
+ }
33
+
34
+ hasTool(toolId: string): boolean {
35
+ return this.tools.has(toolId);
36
+ }
37
+
38
+ dispose() {
39
+ this.tools.clear();
40
+ }
41
+ }
@@ -0,0 +1,176 @@
1
+ import { ToolContribution } from "../contribution";
2
+ import Disposable from "../disposable";
3
+ import { Service } from "../service";
4
+ import CommandService from "./CommandService";
5
+ import ToolRegistryService from "./ToolRegistryService";
6
+
7
+ export type ToolSessionStatus = "idle" | "active";
8
+
9
+ export interface ToolSessionState {
10
+ toolId: string;
11
+ status: ToolSessionStatus;
12
+ dirty: boolean;
13
+ startedAt?: number;
14
+ lastUpdatedAt?: number;
15
+ }
16
+
17
+ export type LeaveDecision = "allow" | "blocked";
18
+
19
+ export interface LeaveResult {
20
+ decision: LeaveDecision;
21
+ reason?: string;
22
+ }
23
+
24
+ export default class ToolSessionService implements Service {
25
+ private readonly sessions = new Map<string, ToolSessionState>();
26
+ private commandService?: CommandService;
27
+ private toolRegistry?: ToolRegistryService;
28
+
29
+ setCommandService(commandService: CommandService) {
30
+ this.commandService = commandService;
31
+ }
32
+
33
+ setToolRegistry(toolRegistry: ToolRegistryService) {
34
+ this.toolRegistry = toolRegistry;
35
+ }
36
+
37
+ registerDirtyTracker(toolId: string, callback: () => boolean): Disposable {
38
+ const wrapped = () => {
39
+ try {
40
+ return callback();
41
+ } catch {
42
+ return false;
43
+ }
44
+ };
45
+ this.dirtyTrackers.set(toolId, wrapped);
46
+ return {
47
+ dispose: () => {
48
+ if (this.dirtyTrackers.get(toolId) === wrapped) {
49
+ this.dirtyTrackers.delete(toolId);
50
+ }
51
+ },
52
+ };
53
+ }
54
+
55
+ private readonly dirtyTrackers = new Map<string, () => boolean>();
56
+
57
+ private ensureSession(toolId: string): ToolSessionState {
58
+ const existing = this.sessions.get(toolId);
59
+ if (existing) return existing;
60
+
61
+ const created: ToolSessionState = {
62
+ toolId,
63
+ status: "idle",
64
+ dirty: false,
65
+ };
66
+ this.sessions.set(toolId, created);
67
+ return created;
68
+ }
69
+
70
+ getState(toolId: string): ToolSessionState {
71
+ return { ...this.ensureSession(toolId) };
72
+ }
73
+
74
+ isDirty(toolId: string): boolean {
75
+ const tracker = this.dirtyTrackers.get(toolId);
76
+ if (tracker) return tracker();
77
+ return this.ensureSession(toolId).dirty;
78
+ }
79
+
80
+ markDirty(toolId: string, dirty = true) {
81
+ const session = this.ensureSession(toolId);
82
+ session.dirty = dirty;
83
+ session.lastUpdatedAt = Date.now();
84
+ }
85
+
86
+ private resolveTool(toolId: string): ToolContribution | undefined {
87
+ return this.toolRegistry?.getTool(toolId);
88
+ }
89
+
90
+ private async runCommand(commandId: string | undefined, ...args: any[]) {
91
+ if (!commandId || !this.commandService) return undefined;
92
+ return await this.commandService.executeCommand(commandId, ...args);
93
+ }
94
+
95
+ async begin(toolId: string): Promise<void> {
96
+ const tool = this.resolveTool(toolId);
97
+ const session = this.ensureSession(toolId);
98
+ if (session.status === "active") return;
99
+
100
+ await this.runCommand(tool?.commands?.begin);
101
+ session.status = "active";
102
+ session.startedAt = Date.now();
103
+ session.lastUpdatedAt = session.startedAt;
104
+ }
105
+
106
+ async validate(toolId: string): Promise<{ ok: boolean; result?: any }> {
107
+ const tool = this.resolveTool(toolId);
108
+ if (!tool?.commands?.validate) {
109
+ return { ok: true };
110
+ }
111
+ const result = await this.runCommand(tool.commands.validate);
112
+ if (result === false) return { ok: false, result };
113
+ if (result && typeof result === "object" && "ok" in result) {
114
+ return { ok: Boolean((result as any).ok), result };
115
+ }
116
+ return { ok: true, result };
117
+ }
118
+
119
+ async commit(toolId: string): Promise<{ ok: boolean; result?: any }> {
120
+ const tool = this.resolveTool(toolId);
121
+ const validateResult = await this.validate(toolId);
122
+ if (!validateResult.ok) return validateResult;
123
+
124
+ const result = await this.runCommand(tool?.commands?.commit);
125
+ const session = this.ensureSession(toolId);
126
+ session.dirty = false;
127
+ session.status = "idle";
128
+ session.lastUpdatedAt = Date.now();
129
+ return { ok: true, result };
130
+ }
131
+
132
+ async rollback(toolId: string): Promise<void> {
133
+ const tool = this.resolveTool(toolId);
134
+ await this.runCommand(tool?.commands?.rollback || tool?.commands?.reset);
135
+ const session = this.ensureSession(toolId);
136
+ session.dirty = false;
137
+ session.status = "idle";
138
+ session.lastUpdatedAt = Date.now();
139
+ }
140
+
141
+ deactivateSession(toolId: string) {
142
+ const session = this.ensureSession(toolId);
143
+ session.status = "idle";
144
+ session.lastUpdatedAt = Date.now();
145
+ }
146
+
147
+ async handleBeforeLeave(toolId: string): Promise<LeaveResult> {
148
+ const tool = this.resolveTool(toolId);
149
+ if (!tool) return { decision: "allow" };
150
+ if (tool.interaction !== "session") return { decision: "allow" };
151
+
152
+ const dirty = this.isDirty(toolId);
153
+ if (!dirty) return { decision: "allow" };
154
+
155
+ const leavePolicy = tool.session?.leavePolicy ?? "block";
156
+ if (leavePolicy === "commit") {
157
+ const committed = await this.commit(toolId);
158
+ if (!committed.ok) {
159
+ return { decision: "blocked", reason: "session-validation-failed" };
160
+ }
161
+ return { decision: "allow" };
162
+ }
163
+
164
+ if (leavePolicy === "rollback") {
165
+ await this.rollback(toolId);
166
+ return { decision: "allow" };
167
+ }
168
+
169
+ return { decision: "blocked", reason: "session-dirty" };
170
+ }
171
+
172
+ dispose() {
173
+ this.sessions.clear();
174
+ this.dirtyTrackers.clear();
175
+ }
176
+ }