@way_marks/server 0.8.0 → 1.0.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.
@@ -49,6 +49,40 @@ exports.getActionsWithFiltering = getActionsWithFiltering;
49
49
  exports.archiveOldActions = archiveOldActions;
50
50
  exports.getArchivedActionsWithFiltering = getArchivedActionsWithFiltering;
51
51
  exports.getSummaryStats = getSummaryStats;
52
+ exports.createSession = createSession;
53
+ exports.getSession = getSession;
54
+ exports.getAllSessions = getAllSessions;
55
+ exports.getSessionActions = getSessionActions;
56
+ exports.rollbackSession = rollbackSession;
57
+ exports.markSessionRolledBack = markSessionRolledBack;
58
+ exports.addTeamMember = addTeamMember;
59
+ exports.getTeamMember = getTeamMember;
60
+ exports.getTeamMemberByEmail = getTeamMemberByEmail;
61
+ exports.getAllTeamMembers = getAllTeamMembers;
62
+ exports.removeTeamMember = removeTeamMember;
63
+ exports.addApprovalRoute = addApprovalRoute;
64
+ exports.getApprovalRoute = getApprovalRoute;
65
+ exports.getAllApprovalRoutes = getAllApprovalRoutes;
66
+ exports.updateApprovalRoute = updateApprovalRoute;
67
+ exports.deleteApprovalRoute = deleteApprovalRoute;
68
+ exports.createApprovalRequest = createApprovalRequest;
69
+ exports.getApprovalRequest = getApprovalRequest;
70
+ exports.getSessionApprovalRequests = getSessionApprovalRequests;
71
+ exports.getPendingApprovals = getPendingApprovals;
72
+ exports.submitApprovalDecision = submitApprovalDecision;
73
+ exports.getApprovalDecisions = getApprovalDecisions;
74
+ exports.addEscalationRule = addEscalationRule;
75
+ exports.getEscalationRule = getEscalationRule;
76
+ exports.getAllEscalationRules = getAllEscalationRules;
77
+ exports.updateEscalationRule = updateEscalationRule;
78
+ exports.deleteEscalationRule = deleteEscalationRule;
79
+ exports.createEscalationRequest = createEscalationRequest;
80
+ exports.getEscalationRequest = getEscalationRequest;
81
+ exports.getPendingEscalations = getPendingEscalations;
82
+ exports.getStaleApprovals = getStaleApprovals;
83
+ exports.submitEscalationDecision = submitEscalationDecision;
84
+ exports.getEscalationDecisions = getEscalationDecisions;
85
+ exports.getEscalationHistory = getEscalationHistory;
52
86
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
53
87
  const fs = __importStar(require("fs"));
54
88
  const path = __importStar(require("path"));
@@ -138,6 +172,30 @@ try {
138
172
  db.exec("ALTER TABLE action_log ADD COLUMN source TEXT DEFAULT 'mcp'");
139
173
  }
140
174
  catch { }
175
+ // Migrate v6: Phase 1 — Session-level rollback
176
+ try {
177
+ db.exec('ALTER TABLE action_log ADD COLUMN rollback_group TEXT');
178
+ }
179
+ catch { }
180
+ try {
181
+ db.exec('ALTER TABLE action_log ADD COLUMN is_reversible INTEGER DEFAULT 1');
182
+ }
183
+ catch { }
184
+ try {
185
+ db.exec('ALTER TABLE action_log ADD COLUMN revert_action_id TEXT');
186
+ }
187
+ catch { }
188
+ // Phase 1: Create sessions table
189
+ db.exec(`
190
+ CREATE TABLE IF NOT EXISTS sessions (
191
+ session_id TEXT PRIMARY KEY,
192
+ user_id TEXT,
193
+ project_id TEXT,
194
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
195
+ rolled_back_at DATETIME,
196
+ status TEXT NOT NULL DEFAULT 'active'
197
+ )
198
+ `);
141
199
  // Indexes for query performance
142
200
  try {
143
201
  db.exec('CREATE INDEX IF NOT EXISTS idx_action_id ON action_log(action_id)');
@@ -192,9 +250,25 @@ db.exec(`
192
250
  event_type TEXT NOT NULL DEFAULT 'execution',
193
251
  observation_context TEXT,
194
252
  request_source TEXT DEFAULT 'direct',
195
- source TEXT DEFAULT 'mcp'
253
+ source TEXT DEFAULT 'mcp',
254
+ rollback_group TEXT,
255
+ is_reversible INTEGER DEFAULT 1,
256
+ revert_action_id TEXT
196
257
  )
197
258
  `);
259
+ // Phase 1: Add same columns to archive (migrations)
260
+ try {
261
+ db.exec('ALTER TABLE action_archive ADD COLUMN rollback_group TEXT');
262
+ }
263
+ catch { }
264
+ try {
265
+ db.exec('ALTER TABLE action_archive ADD COLUMN is_reversible INTEGER DEFAULT 1');
266
+ }
267
+ catch { }
268
+ try {
269
+ db.exec('ALTER TABLE action_archive ADD COLUMN revert_action_id TEXT');
270
+ }
271
+ catch { }
198
272
  // Phase 3: Add indexes on archive table
199
273
  try {
200
274
  db.exec('CREATE INDEX IF NOT EXISTS idx_archive_action_id ON action_archive(action_id)');
@@ -204,6 +278,174 @@ try {
204
278
  db.exec('CREATE INDEX IF NOT EXISTS idx_archive_created_at ON action_archive(created_at DESC)');
205
279
  }
206
280
  catch { }
281
+ // Phase 1: Add indexes for session rollback
282
+ try {
283
+ db.exec('CREATE INDEX IF NOT EXISTS idx_action_session ON action_log(session_id, status)');
284
+ }
285
+ catch { }
286
+ try {
287
+ db.exec('CREATE INDEX IF NOT EXISTS idx_action_rollback_group ON action_log(rollback_group)');
288
+ }
289
+ catch { }
290
+ try {
291
+ db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status, created_at DESC)');
292
+ }
293
+ catch { }
294
+ try {
295
+ db.exec('CREATE INDEX IF NOT EXISTS idx_archive_session ON action_archive(session_id)');
296
+ }
297
+ catch { }
298
+ // Phase 2: Create team_members table
299
+ db.exec(`
300
+ CREATE TABLE IF NOT EXISTS team_members (
301
+ member_id TEXT PRIMARY KEY,
302
+ name TEXT NOT NULL,
303
+ email TEXT NOT NULL UNIQUE,
304
+ slack_id TEXT,
305
+ role TEXT DEFAULT 'approver',
306
+ added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
307
+ added_by TEXT,
308
+ status TEXT DEFAULT 'active'
309
+ )
310
+ `);
311
+ // Phase 2: Create approval_routes table (rules for who approves what)
312
+ db.exec(`
313
+ CREATE TABLE IF NOT EXISTS approval_routes (
314
+ route_id TEXT PRIMARY KEY,
315
+ name TEXT NOT NULL,
316
+ description TEXT,
317
+ condition_type TEXT DEFAULT 'all_sessions',
318
+ condition_json TEXT,
319
+ required_approvers INTEGER DEFAULT 1,
320
+ approver_ids TEXT NOT NULL,
321
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
322
+ created_by TEXT,
323
+ status TEXT DEFAULT 'active'
324
+ )
325
+ `);
326
+ // Phase 2: Create approval_requests table (pending/completed approvals)
327
+ db.exec(`
328
+ CREATE TABLE IF NOT EXISTS approval_requests (
329
+ request_id TEXT PRIMARY KEY,
330
+ session_id TEXT NOT NULL,
331
+ route_id TEXT NOT NULL,
332
+ triggered_by TEXT NOT NULL,
333
+ triggered_at DATETIME DEFAULT CURRENT_TIMESTAMP,
334
+ status TEXT DEFAULT 'pending',
335
+ completed_at DATETIME,
336
+ approver_ids TEXT NOT NULL,
337
+ approved_count INTEGER DEFAULT 0,
338
+ rejected_count INTEGER DEFAULT 0,
339
+ approval_details TEXT
340
+ )
341
+ `);
342
+ // Phase 2: Create approval_decisions table (audit trail)
343
+ db.exec(`
344
+ CREATE TABLE IF NOT EXISTS approval_decisions (
345
+ decision_id TEXT PRIMARY KEY,
346
+ request_id TEXT NOT NULL,
347
+ approver_id TEXT NOT NULL,
348
+ decision TEXT NOT NULL,
349
+ reason TEXT,
350
+ decided_at DATETIME DEFAULT CURRENT_TIMESTAMP
351
+ )
352
+ `);
353
+ // Phase 2: Add indexes for team tables
354
+ try {
355
+ db.exec('CREATE INDEX IF NOT EXISTS idx_team_members_email ON team_members(email)');
356
+ }
357
+ catch { }
358
+ try {
359
+ db.exec('CREATE INDEX IF NOT EXISTS idx_team_members_status ON team_members(status)');
360
+ }
361
+ catch { }
362
+ try {
363
+ db.exec('CREATE INDEX IF NOT EXISTS idx_approval_routes_status ON approval_routes(status)');
364
+ }
365
+ catch { }
366
+ try {
367
+ db.exec('CREATE INDEX IF NOT EXISTS idx_approval_requests_session ON approval_requests(session_id)');
368
+ }
369
+ catch { }
370
+ try {
371
+ db.exec('CREATE INDEX IF NOT EXISTS idx_approval_requests_status ON approval_requests(status)');
372
+ }
373
+ catch { }
374
+ try {
375
+ db.exec('CREATE INDEX IF NOT EXISTS idx_approval_requests_triggered ON approval_requests(triggered_at DESC)');
376
+ }
377
+ catch { }
378
+ try {
379
+ db.exec('CREATE INDEX IF NOT EXISTS idx_approval_decisions_request ON approval_decisions(request_id)');
380
+ }
381
+ catch { }
382
+ try {
383
+ db.exec('CREATE INDEX IF NOT EXISTS idx_approval_decisions_approver ON approval_decisions(approver_id)');
384
+ }
385
+ catch { }
386
+ // Phase 3: Create escalation_rules table
387
+ db.exec(`
388
+ CREATE TABLE IF NOT EXISTS escalation_rules (
389
+ rule_id TEXT PRIMARY KEY,
390
+ name TEXT NOT NULL,
391
+ description TEXT,
392
+ timeout_hours INTEGER DEFAULT 24,
393
+ escalation_targets TEXT NOT NULL,
394
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
395
+ created_by TEXT,
396
+ status TEXT DEFAULT 'active'
397
+ )
398
+ `);
399
+ // Phase 3: Create escalation_requests table
400
+ db.exec(`
401
+ CREATE TABLE IF NOT EXISTS escalation_requests (
402
+ request_id TEXT PRIMARY KEY,
403
+ approval_request_id TEXT NOT NULL,
404
+ session_id TEXT NOT NULL,
405
+ escalation_triggered_at DATETIME,
406
+ escalation_deadline DATETIME,
407
+ escalation_targets TEXT NOT NULL,
408
+ status TEXT DEFAULT 'pending',
409
+ decided_at DATETIME,
410
+ decision TEXT
411
+ )
412
+ `);
413
+ // Phase 3: Create escalation_decisions table
414
+ db.exec(`
415
+ CREATE TABLE IF NOT EXISTS escalation_decisions (
416
+ decision_id TEXT PRIMARY KEY,
417
+ escalation_request_id TEXT NOT NULL,
418
+ target_id TEXT NOT NULL,
419
+ decision TEXT NOT NULL,
420
+ reason TEXT,
421
+ decided_at DATETIME DEFAULT CURRENT_TIMESTAMP
422
+ )
423
+ `);
424
+ // Phase 3: Add indexes for escalation tables
425
+ try {
426
+ db.exec('CREATE INDEX IF NOT EXISTS idx_escalation_rules_status ON escalation_rules(status)');
427
+ }
428
+ catch { }
429
+ try {
430
+ db.exec('CREATE INDEX IF NOT EXISTS idx_escalation_requests_approval ON escalation_requests(approval_request_id)');
431
+ }
432
+ catch { }
433
+ try {
434
+ db.exec('CREATE INDEX IF NOT EXISTS idx_escalation_requests_deadline ON escalation_requests(escalation_deadline)');
435
+ }
436
+ catch { }
437
+ try {
438
+ db.exec('CREATE INDEX IF NOT EXISTS idx_escalation_requests_status ON escalation_requests(status)');
439
+ }
440
+ catch { }
441
+ try {
442
+ db.exec('CREATE INDEX IF NOT EXISTS idx_escalation_decisions_request ON escalation_decisions(escalation_request_id)');
443
+ }
444
+ catch { }
445
+ try {
446
+ db.exec('CREATE INDEX IF NOT EXISTS idx_escalation_decisions_target ON escalation_decisions(target_id)');
447
+ }
448
+ catch { }
207
449
  const insertStmt = db.prepare(`
208
450
  INSERT INTO action_log (action_id, session_id, tool_name, target_path, input_payload, before_snapshot, status, decision, policy_reason, matched_rule, event_type, observation_context, request_source, source)
209
451
  VALUES (@action_id, @session_id, @tool_name, @target_path, @input_payload, @before_snapshot, @status, @decision, @policy_reason, @matched_rule, @event_type, @observation_context, @request_source, @source)
@@ -441,4 +683,305 @@ function getSummaryStats() {
441
683
  topPaths,
442
684
  };
443
685
  }
686
+ // Phase 1: Session management functions
687
+ function createSession(session_id, user_id, project_id) {
688
+ db.prepare(`
689
+ INSERT OR IGNORE INTO sessions (session_id, user_id, project_id, status)
690
+ VALUES (?, ?, ?, 'active')
691
+ `).run(session_id, user_id ?? null, project_id ?? null);
692
+ }
693
+ function getSession(session_id) {
694
+ return db.prepare(`
695
+ SELECT * FROM sessions WHERE session_id = ?
696
+ `).get(session_id);
697
+ }
698
+ function getAllSessions() {
699
+ return db.prepare(`
700
+ SELECT * FROM sessions ORDER BY created_at DESC
701
+ `).all();
702
+ }
703
+ function getSessionActions(session_id) {
704
+ return db.prepare(`
705
+ SELECT * FROM action_log
706
+ WHERE session_id = ?
707
+ ORDER BY created_at ASC
708
+ `).all(session_id);
709
+ }
710
+ function rollbackSession(session_id) {
711
+ try {
712
+ // Start transaction
713
+ const transaction = db.transaction(() => {
714
+ // Get all actions in session
715
+ const actions = getSessionActions(session_id);
716
+ if (actions.length === 0) {
717
+ throw new Error(`Session ${session_id} has no actions to rollback`);
718
+ }
719
+ // Check if all actions are reversible
720
+ const nonReversible = actions.filter(a => a.is_reversible === 0);
721
+ if (nonReversible.length > 0) {
722
+ throw new Error(`${nonReversible.length} action(s) in session are not reversible`);
723
+ }
724
+ // For each action, perform rollback
725
+ let rolledBackCount = 0;
726
+ for (const action of actions) {
727
+ // Mark as rolled back
728
+ markRolledBack(action.action_id);
729
+ rolledBackCount++;
730
+ // If before_snapshot exists, restore it (for write_file actions)
731
+ if (action.before_snapshot) {
732
+ try {
733
+ const snapshot = JSON.parse(action.before_snapshot);
734
+ if (snapshot.file_path && snapshot.content !== undefined) {
735
+ // This will be handled by the rollback manager
736
+ // We just mark it as rolled back here
737
+ }
738
+ }
739
+ catch { }
740
+ }
741
+ }
742
+ // Mark session as rolled back
743
+ db.prepare(`
744
+ UPDATE sessions
745
+ SET rolled_back_at = datetime('now'), status = 'rolled_back'
746
+ WHERE session_id = ?
747
+ `).run(session_id);
748
+ return rolledBackCount;
749
+ });
750
+ const actionsRolledBack = transaction();
751
+ return {
752
+ success: true,
753
+ session_id,
754
+ actions_rolled_back: actionsRolledBack,
755
+ };
756
+ }
757
+ catch (error) {
758
+ return {
759
+ success: false,
760
+ session_id,
761
+ actions_rolled_back: 0,
762
+ error: error.message,
763
+ };
764
+ }
765
+ }
766
+ function markSessionRolledBack(session_id) {
767
+ db.prepare(`
768
+ UPDATE sessions
769
+ SET rolled_back_at = datetime('now'), status = 'rolled_back'
770
+ WHERE session_id = ?
771
+ `).run(session_id);
772
+ }
773
+ // Phase 2: Team member functions
774
+ function addTeamMember(member_id, name, email, added_by, slack_id) {
775
+ db.prepare(`
776
+ INSERT OR REPLACE INTO team_members (member_id, name, email, slack_id, added_by, status)
777
+ VALUES (?, ?, ?, ?, ?, 'active')
778
+ `).run(member_id, name, email, slack_id ?? null, added_by);
779
+ }
780
+ function getTeamMember(member_id) {
781
+ return db.prepare(`
782
+ SELECT * FROM team_members WHERE member_id = ?
783
+ `).get(member_id);
784
+ }
785
+ function getTeamMemberByEmail(email) {
786
+ return db.prepare(`
787
+ SELECT * FROM team_members WHERE email = ?
788
+ `).get(email);
789
+ }
790
+ function getAllTeamMembers() {
791
+ return db.prepare(`
792
+ SELECT * FROM team_members WHERE status = 'active' ORDER BY name ASC
793
+ `).all();
794
+ }
795
+ function removeTeamMember(member_id) {
796
+ db.prepare(`
797
+ UPDATE team_members SET status = 'inactive' WHERE member_id = ?
798
+ `).run(member_id);
799
+ }
800
+ // Phase 2: Approval route functions
801
+ function addApprovalRoute(route_id, name, approver_ids, created_by, description, condition_type, condition_json) {
802
+ db.prepare(`
803
+ INSERT OR REPLACE INTO approval_routes (route_id, name, description, condition_type, condition_json, approver_ids, created_by, status)
804
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'active')
805
+ `).run(route_id, name, description ?? null, condition_type ?? 'all_sessions', condition_json ?? null, JSON.stringify(approver_ids), created_by);
806
+ }
807
+ function getApprovalRoute(route_id) {
808
+ return db.prepare(`
809
+ SELECT * FROM approval_routes WHERE route_id = ?
810
+ `).get(route_id);
811
+ }
812
+ function getAllApprovalRoutes() {
813
+ return db.prepare(`
814
+ SELECT * FROM approval_routes WHERE status = 'active' ORDER BY name ASC
815
+ `).all();
816
+ }
817
+ function updateApprovalRoute(route_id, updates) {
818
+ const current = getApprovalRoute(route_id);
819
+ if (!current)
820
+ throw new Error(`Route ${route_id} not found`);
821
+ db.prepare(`
822
+ UPDATE approval_routes
823
+ SET name = ?, description = ?, approver_ids = ?
824
+ WHERE route_id = ?
825
+ `).run(updates.name ?? current.name, updates.description ?? current.description, updates.approver_ids ? JSON.stringify(updates.approver_ids) : current.approver_ids, route_id);
826
+ }
827
+ function deleteApprovalRoute(route_id) {
828
+ db.prepare(`
829
+ UPDATE approval_routes SET status = 'inactive' WHERE route_id = ?
830
+ `).run(route_id);
831
+ }
832
+ // Phase 2: Approval request functions
833
+ function createApprovalRequest(request_id, session_id, route_id, triggered_by, approver_ids) {
834
+ db.prepare(`
835
+ INSERT INTO approval_requests (request_id, session_id, route_id, triggered_by, approver_ids, status)
836
+ VALUES (?, ?, ?, ?, ?, 'pending')
837
+ `).run(request_id, session_id, route_id, triggered_by, JSON.stringify(approver_ids));
838
+ }
839
+ function getApprovalRequest(request_id) {
840
+ return db.prepare(`
841
+ SELECT * FROM approval_requests WHERE request_id = ?
842
+ `).get(request_id);
843
+ }
844
+ function getSessionApprovalRequests(session_id) {
845
+ return db.prepare(`
846
+ SELECT * FROM approval_requests WHERE session_id = ? ORDER BY triggered_at DESC
847
+ `).all(session_id);
848
+ }
849
+ function getPendingApprovals(approver_id) {
850
+ if (approver_id) {
851
+ return db.prepare(`
852
+ SELECT * FROM approval_requests
853
+ WHERE status = 'pending' AND approver_ids LIKE ?
854
+ ORDER BY triggered_at DESC
855
+ `).all(`%"${approver_id}"%`);
856
+ }
857
+ return db.prepare(`
858
+ SELECT * FROM approval_requests WHERE status = 'pending' ORDER BY triggered_at DESC
859
+ `).all();
860
+ }
861
+ function submitApprovalDecision(decision_id, request_id, approver_id, decision, reason) {
862
+ // Insert decision
863
+ db.prepare(`
864
+ INSERT INTO approval_decisions (decision_id, request_id, approver_id, decision, reason)
865
+ VALUES (?, ?, ?, ?, ?)
866
+ `).run(decision_id, request_id, approver_id, decision, reason ?? null);
867
+ // Update approval request counts
868
+ const request = getApprovalRequest(request_id);
869
+ if (!request)
870
+ throw new Error(`Request ${request_id} not found`);
871
+ const decisions = db.prepare(`
872
+ SELECT decision FROM approval_decisions WHERE request_id = ?
873
+ `).all(request_id);
874
+ const approvedCount = decisions.filter(d => d.decision === 'approve').length;
875
+ const rejectedCount = decisions.filter(d => d.decision === 'reject').length;
876
+ const approverIds = JSON.parse(request.approver_ids);
877
+ // Determine new status
878
+ let newStatus = 'pending';
879
+ if (rejectedCount > 0) {
880
+ newStatus = 'rejected';
881
+ }
882
+ else if (approvedCount >= approverIds.length) {
883
+ newStatus = 'approved';
884
+ }
885
+ db.prepare(`
886
+ UPDATE approval_requests
887
+ SET approved_count = ?, rejected_count = ?, status = ?, completed_at = ?
888
+ WHERE request_id = ?
889
+ `).run(approvedCount, rejectedCount, newStatus, newStatus !== 'pending' ? new Date().toISOString() : null, request_id);
890
+ }
891
+ function getApprovalDecisions(request_id) {
892
+ return db.prepare(`
893
+ SELECT * FROM approval_decisions WHERE request_id = ? ORDER BY decided_at ASC
894
+ `).all(request_id);
895
+ }
896
+ // Phase 3: Escalation rule functions
897
+ function addEscalationRule(rule_id, name, escalation_targets, created_by, description, timeout_hours) {
898
+ db.prepare(`
899
+ INSERT OR REPLACE INTO escalation_rules (rule_id, name, description, timeout_hours, escalation_targets, created_by, status)
900
+ VALUES (?, ?, ?, ?, ?, ?, 'active')
901
+ `).run(rule_id, name, description ?? null, timeout_hours ?? 24, JSON.stringify(escalation_targets), created_by);
902
+ }
903
+ function getEscalationRule(rule_id) {
904
+ return db.prepare(`
905
+ SELECT * FROM escalation_rules WHERE rule_id = ?
906
+ `).get(rule_id);
907
+ }
908
+ function getAllEscalationRules() {
909
+ return db.prepare(`
910
+ SELECT * FROM escalation_rules WHERE status = 'active' ORDER BY name ASC
911
+ `).all();
912
+ }
913
+ function updateEscalationRule(rule_id, updates) {
914
+ const current = getEscalationRule(rule_id);
915
+ if (!current)
916
+ throw new Error(`Rule ${rule_id} not found`);
917
+ db.prepare(`
918
+ UPDATE escalation_rules
919
+ SET name = ?, description = ?, timeout_hours = ?, escalation_targets = ?
920
+ WHERE rule_id = ?
921
+ `).run(updates.name ?? current.name, updates.description ?? current.description, updates.timeout_hours ?? current.timeout_hours, updates.escalation_targets ? JSON.stringify(updates.escalation_targets) : current.escalation_targets, rule_id);
922
+ }
923
+ function deleteEscalationRule(rule_id) {
924
+ db.prepare(`
925
+ UPDATE escalation_rules SET status = 'inactive' WHERE rule_id = ?
926
+ `).run(rule_id);
927
+ }
928
+ // Phase 3: Escalation request functions
929
+ function createEscalationRequest(request_id, approval_request_id, session_id, escalation_targets, deadline) {
930
+ db.prepare(`
931
+ INSERT INTO escalation_requests (request_id, approval_request_id, session_id, escalation_triggered_at, escalation_deadline, escalation_targets, status)
932
+ VALUES (?, ?, ?, datetime('now'), ?, ?, 'pending')
933
+ `).run(request_id, approval_request_id, session_id, deadline, JSON.stringify(escalation_targets));
934
+ }
935
+ function getEscalationRequest(request_id) {
936
+ return db.prepare(`
937
+ SELECT * FROM escalation_requests WHERE request_id = ?
938
+ `).get(request_id);
939
+ }
940
+ function getPendingEscalations() {
941
+ return db.prepare(`
942
+ SELECT * FROM escalation_requests WHERE status = 'pending' ORDER BY escalation_deadline ASC
943
+ `).all();
944
+ }
945
+ function getStaleApprovals(now) {
946
+ return db.prepare(`
947
+ SELECT DISTINCT ar.request_id as approval_request_id, ar.session_id
948
+ FROM approval_requests ar
949
+ WHERE ar.status = 'pending'
950
+ AND ar.triggered_at < ?
951
+ AND NOT EXISTS (
952
+ SELECT 1 FROM escalation_requests er
953
+ WHERE er.approval_request_id = ar.request_id
954
+ )
955
+ `).all(now);
956
+ }
957
+ function submitEscalationDecision(decision_id, escalation_request_id, target_id, decision, reason) {
958
+ db.prepare(`
959
+ INSERT INTO escalation_decisions (decision_id, escalation_request_id, target_id, decision, reason)
960
+ VALUES (?, ?, ?, ?, ?)
961
+ `).run(decision_id, escalation_request_id, target_id, decision, reason ?? null);
962
+ // Update escalation request status if all targets have decided
963
+ const request = getEscalationRequest(escalation_request_id);
964
+ if (!request)
965
+ return;
966
+ const decisions = db.prepare(`
967
+ SELECT DISTINCT decision FROM escalation_decisions WHERE escalation_request_id = ?
968
+ `).all(escalation_request_id);
969
+ const hasBlockDecision = decisions.some(d => d.decision === 'block');
970
+ const newStatus = hasBlockDecision ? 'blocked' : 'proceeded';
971
+ db.prepare(`
972
+ UPDATE escalation_requests
973
+ SET status = ?, decided_at = datetime('now'), decision = ?
974
+ WHERE request_id = ?
975
+ `).run(newStatus, newStatus, escalation_request_id);
976
+ }
977
+ function getEscalationDecisions(escalation_request_id) {
978
+ return db.prepare(`
979
+ SELECT * FROM escalation_decisions WHERE escalation_request_id = ? ORDER BY decided_at ASC
980
+ `).all(escalation_request_id);
981
+ }
982
+ function getEscalationHistory(session_id) {
983
+ return db.prepare(`
984
+ SELECT * FROM escalation_requests WHERE session_id = ? ORDER BY escalation_triggered_at DESC
985
+ `).all(session_id);
986
+ }
444
987
  exports.default = db;