run-dmcp 0.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.
Files changed (167) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +79 -0
  3. package/dist/__tests__/engineVocabulary.test.d.ts +1 -0
  4. package/dist/__tests__/engineVocabulary.test.js +147 -0
  5. package/dist/db/__tests__/connection.test.d.ts +1 -0
  6. package/dist/db/__tests__/connection.test.js +72 -0
  7. package/dist/db/__tests__/testDb.d.ts +33 -0
  8. package/dist/db/__tests__/testDb.js +41 -0
  9. package/dist/db/connection.d.ts +22 -0
  10. package/dist/db/connection.js +107 -0
  11. package/dist/db/schema.d.ts +1 -0
  12. package/dist/db/schema.js +725 -0
  13. package/dist/events/emitter.d.ts +22 -0
  14. package/dist/events/emitter.js +71 -0
  15. package/dist/http/server.d.ts +3 -0
  16. package/dist/http/server.js +649 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +92 -0
  19. package/dist/register/abilities.d.ts +2 -0
  20. package/dist/register/abilities.js +165 -0
  21. package/dist/register/audio.d.ts +2 -0
  22. package/dist/register/audio.js +326 -0
  23. package/dist/register/batch.d.ts +2 -0
  24. package/dist/register/batch.js +343 -0
  25. package/dist/register/character.d.ts +2 -0
  26. package/dist/register/character.js +324 -0
  27. package/dist/register/combat.d.ts +2 -0
  28. package/dist/register/combat.js +207 -0
  29. package/dist/register/core.d.ts +2 -0
  30. package/dist/register/core.js +1040 -0
  31. package/dist/register/display.d.ts +2 -0
  32. package/dist/register/display.js +263 -0
  33. package/dist/register/factions.d.ts +2 -0
  34. package/dist/register/factions.js +186 -0
  35. package/dist/register/images.d.ts +2 -0
  36. package/dist/register/images.js +400 -0
  37. package/dist/register/inventory.d.ts +2 -0
  38. package/dist/register/inventory.js +115 -0
  39. package/dist/register/mcp-prompts.d.ts +2 -0
  40. package/dist/register/mcp-prompts.js +684 -0
  41. package/dist/register/mcp-resources.d.ts +2 -0
  42. package/dist/register/mcp-resources.js +335 -0
  43. package/dist/register/narrative.d.ts +2 -0
  44. package/dist/register/narrative.js +242 -0
  45. package/dist/register/notes.d.ts +2 -0
  46. package/dist/register/notes.js +170 -0
  47. package/dist/register/pause.d.ts +2 -0
  48. package/dist/register/pause.js +580 -0
  49. package/dist/register/quests.d.ts +2 -0
  50. package/dist/register/quests.js +118 -0
  51. package/dist/register/relationships.d.ts +2 -0
  52. package/dist/register/relationships.js +147 -0
  53. package/dist/register/resources.d.ts +2 -0
  54. package/dist/register/resources.js +277 -0
  55. package/dist/register/secrets.d.ts +2 -0
  56. package/dist/register/secrets.js +192 -0
  57. package/dist/register/status.d.ts +2 -0
  58. package/dist/register/status.js +130 -0
  59. package/dist/register/tables.d.ts +2 -0
  60. package/dist/register/tables.js +146 -0
  61. package/dist/register/tags.d.ts +2 -0
  62. package/dist/register/tags.js +114 -0
  63. package/dist/register/time.d.ts +2 -0
  64. package/dist/register/time.js +281 -0
  65. package/dist/register/world.d.ts +2 -0
  66. package/dist/register/world.js +127 -0
  67. package/dist/schemas/index.d.ts +921 -0
  68. package/dist/schemas/index.js +121 -0
  69. package/dist/test-setup.d.ts +1 -0
  70. package/dist/test-setup.js +13 -0
  71. package/dist/tools/__tests__/audio.test.d.ts +1 -0
  72. package/dist/tools/__tests__/audio.test.js +59 -0
  73. package/dist/tools/__tests__/conserved.test.d.ts +1 -0
  74. package/dist/tools/__tests__/conserved.test.js +488 -0
  75. package/dist/tools/__tests__/constraint.test.d.ts +1 -0
  76. package/dist/tools/__tests__/constraint.test.js +212 -0
  77. package/dist/tools/__tests__/expiry-consequences.test.d.ts +1 -0
  78. package/dist/tools/__tests__/expiry-consequences.test.js +110 -0
  79. package/dist/tools/__tests__/images.test.d.ts +1 -0
  80. package/dist/tools/__tests__/images.test.js +59 -0
  81. package/dist/tools/__tests__/relationship.test.d.ts +1 -0
  82. package/dist/tools/__tests__/relationship.test.js +132 -0
  83. package/dist/tools/__tests__/resource-constraints.test.d.ts +1 -0
  84. package/dist/tools/__tests__/resource-constraints.test.js +131 -0
  85. package/dist/tools/__tests__/resource.test.d.ts +1 -0
  86. package/dist/tools/__tests__/resource.test.js +190 -0
  87. package/dist/tools/__tests__/time.test.d.ts +1 -0
  88. package/dist/tools/__tests__/time.test.js +404 -0
  89. package/dist/tools/__tests__/timers.test.d.ts +1 -0
  90. package/dist/tools/__tests__/timers.test.js +426 -0
  91. package/dist/tools/__tests__/world.test.d.ts +1 -0
  92. package/dist/tools/__tests__/world.test.js +70 -0
  93. package/dist/tools/ability.d.ts +48 -0
  94. package/dist/tools/ability.js +238 -0
  95. package/dist/tools/audio.d.ts +24 -0
  96. package/dist/tools/audio.js +365 -0
  97. package/dist/tools/character.d.ts +70 -0
  98. package/dist/tools/character.js +309 -0
  99. package/dist/tools/combat.d.ts +13 -0
  100. package/dist/tools/combat.js +195 -0
  101. package/dist/tools/constraint.d.ts +132 -0
  102. package/dist/tools/constraint.js +269 -0
  103. package/dist/tools/dice.d.ts +23 -0
  104. package/dist/tools/dice.js +111 -0
  105. package/dist/tools/display.d.ts +120 -0
  106. package/dist/tools/display.js +528 -0
  107. package/dist/tools/faction.d.ts +61 -0
  108. package/dist/tools/faction.js +269 -0
  109. package/dist/tools/game.d.ts +96 -0
  110. package/dist/tools/game.js +526 -0
  111. package/dist/tools/image-prompt.d.ts +49 -0
  112. package/dist/tools/image-prompt.js +479 -0
  113. package/dist/tools/images.d.ts +47 -0
  114. package/dist/tools/images.js +449 -0
  115. package/dist/tools/inventory.d.ts +20 -0
  116. package/dist/tools/inventory.js +145 -0
  117. package/dist/tools/narrative.d.ts +58 -0
  118. package/dist/tools/narrative.js +237 -0
  119. package/dist/tools/notes.d.ts +41 -0
  120. package/dist/tools/notes.js +220 -0
  121. package/dist/tools/pause.d.ts +110 -0
  122. package/dist/tools/pause.js +1254 -0
  123. package/dist/tools/quest.d.ts +34 -0
  124. package/dist/tools/quest.js +164 -0
  125. package/dist/tools/relationship.d.ts +74 -0
  126. package/dist/tools/relationship.js +324 -0
  127. package/dist/tools/resource.d.ts +93 -0
  128. package/dist/tools/resource.js +374 -0
  129. package/dist/tools/rules.d.ts +4 -0
  130. package/dist/tools/rules.js +30 -0
  131. package/dist/tools/secrets.d.ts +49 -0
  132. package/dist/tools/secrets.js +195 -0
  133. package/dist/tools/status.d.ts +36 -0
  134. package/dist/tools/status.js +218 -0
  135. package/dist/tools/tables.d.ts +33 -0
  136. package/dist/tools/tables.js +209 -0
  137. package/dist/tools/tags.d.ts +52 -0
  138. package/dist/tools/tags.js +176 -0
  139. package/dist/tools/time.d.ts +33 -0
  140. package/dist/tools/time.js +276 -0
  141. package/dist/tools/timers.d.ts +41 -0
  142. package/dist/tools/timers.js +215 -0
  143. package/dist/tools/world.d.ts +78 -0
  144. package/dist/tools/world.js +331 -0
  145. package/dist/types/index.d.ts +969 -0
  146. package/dist/types/index.js +1 -0
  147. package/dist/utils/__tests__/json.test.d.ts +1 -0
  148. package/dist/utils/__tests__/json.test.js +55 -0
  149. package/dist/utils/__tests__/validation.test.d.ts +1 -0
  150. package/dist/utils/__tests__/validation.test.js +90 -0
  151. package/dist/utils/errors.d.ts +44 -0
  152. package/dist/utils/errors.js +121 -0
  153. package/dist/utils/json.d.ts +9 -0
  154. package/dist/utils/json.js +23 -0
  155. package/dist/utils/logger.d.ts +7 -0
  156. package/dist/utils/logger.js +50 -0
  157. package/dist/utils/output-schemas.d.ts +594 -0
  158. package/dist/utils/output-schemas.js +331 -0
  159. package/dist/utils/tool-annotations.d.ts +147 -0
  160. package/dist/utils/tool-annotations.js +98 -0
  161. package/dist/utils/validation.d.ts +34 -0
  162. package/dist/utils/validation.js +52 -0
  163. package/dist/utils/verbosity.d.ts +57 -0
  164. package/dist/utils/verbosity.js +67 -0
  165. package/dist/utils/webui.d.ts +20 -0
  166. package/dist/utils/webui.js +35 -0
  167. package/package.json +75 -0
@@ -0,0 +1,1254 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { getDatabase } from "../db/connection.js";
3
+ import { safeJsonParse } from "../utils/json.js";
4
+ // ============================================================================
5
+ // PAUSE PREPARATION
6
+ // ============================================================================
7
+ /**
8
+ * Prepares for a game pause by returning current state, a comprehensive
9
+ * game state audit, persistence reminders, and a checklist of context
10
+ * that should be saved. This helps agents understand what needs to be
11
+ * captured before ending a game.
12
+ */
13
+ export function preparePause(gameId) {
14
+ const db = getDatabase();
15
+ // Get game
16
+ const gameRow = db
17
+ .prepare(`SELECT * FROM games WHERE id = ?`)
18
+ .get(gameId);
19
+ if (!gameRow)
20
+ return null;
21
+ // ============================================================================
22
+ // COMPREHENSIVE GAME STATE AUDIT
23
+ // ============================================================================
24
+ // Characters
25
+ const characterStats = db
26
+ .prepare(`
27
+ SELECT
28
+ COUNT(*) as total,
29
+ SUM(CASE WHEN is_player = 1 THEN 1 ELSE 0 END) as players,
30
+ SUM(CASE WHEN is_player = 0 THEN 1 ELSE 0 END) as npcs,
31
+ SUM(CASE WHEN notes IS NOT NULL AND notes != '' THEN 1 ELSE 0 END) as with_notes
32
+ FROM characters WHERE game_id = ?
33
+ `)
34
+ .get(gameId);
35
+ // Characters with conditions
36
+ const charactersWithConditions = db
37
+ .prepare(`
38
+ SELECT COUNT(DISTINCT c.id) as count
39
+ FROM characters c
40
+ JOIN status_effects se ON se.target_id = c.id
41
+ WHERE c.game_id = ?
42
+ `)
43
+ .get(gameId);
44
+ // Locations
45
+ const locationStats = db
46
+ .prepare(`SELECT COUNT(*) as total FROM locations WHERE game_id = ?`)
47
+ .get(gameId);
48
+ // Count locations that have exits (exits are stored in properties JSON)
49
+ const locationRows = db
50
+ .prepare(`SELECT properties FROM locations WHERE game_id = ?`)
51
+ .all(gameId);
52
+ let connectedCount = 0;
53
+ for (const row of locationRows) {
54
+ const props = safeJsonParse(row.properties, { exits: [] });
55
+ if (props.exits && props.exits.length > 0) {
56
+ connectedCount++;
57
+ }
58
+ }
59
+ const connectedLocations = { count: connectedCount };
60
+ // Quests
61
+ const questStats = db
62
+ .prepare(`
63
+ SELECT
64
+ SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
65
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
66
+ SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed
67
+ FROM quests WHERE game_id = ?
68
+ `)
69
+ .get(gameId);
70
+ // Items
71
+ const itemStats = db
72
+ .prepare(`
73
+ SELECT
74
+ COUNT(*) as total,
75
+ SUM(CASE WHEN owner_type = 'character' THEN 1 ELSE 0 END) as in_inventories,
76
+ SUM(CASE WHEN owner_type = 'location' THEN 1 ELSE 0 END) as in_locations
77
+ FROM items WHERE game_id = ?
78
+ `)
79
+ .get(gameId);
80
+ // Combat
81
+ const combatRow = db
82
+ .prepare(`SELECT * FROM combats WHERE game_id = ? AND status = 'active' LIMIT 1`)
83
+ .get(gameId);
84
+ // Resources
85
+ const resourceStats = db
86
+ .prepare(`
87
+ SELECT
88
+ SUM(CASE WHEN owner_type = 'game' THEN 1 ELSE 0 END) as game_level,
89
+ SUM(CASE WHEN owner_type = 'character' THEN 1 ELSE 0 END) as character_level
90
+ FROM resources WHERE game_id = ?
91
+ `)
92
+ .get(gameId);
93
+ // Timers
94
+ const timerStats = db
95
+ .prepare(`
96
+ SELECT
97
+ SUM(CASE WHEN triggered = 0 THEN 1 ELSE 0 END) as active,
98
+ SUM(CASE WHEN triggered = 1 THEN 1 ELSE 0 END) as triggered
99
+ FROM timers WHERE game_id = ?
100
+ `)
101
+ .get(gameId);
102
+ // Scheduled events
103
+ const eventStats = db
104
+ .prepare(`
105
+ SELECT
106
+ SUM(CASE WHEN triggered = 0 THEN 1 ELSE 0 END) as pending,
107
+ SUM(CASE WHEN triggered = 1 THEN 1 ELSE 0 END) as triggered
108
+ FROM scheduled_events WHERE game_id = ?
109
+ `)
110
+ .get(gameId);
111
+ // Relationships
112
+ const relationshipCount = db
113
+ .prepare(`SELECT COUNT(*) as count FROM relationships WHERE game_id = ?`)
114
+ .get(gameId);
115
+ // Secrets
116
+ const secretStats = db
117
+ .prepare(`
118
+ SELECT
119
+ COUNT(*) as total,
120
+ SUM(CASE WHEN is_public = 1 THEN 1 ELSE 0 END) as revealed
121
+ FROM secrets WHERE game_id = ?
122
+ `)
123
+ .get(gameId);
124
+ // Factions
125
+ const factionStats = db
126
+ .prepare(`
127
+ SELECT
128
+ SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
129
+ SUM(CASE WHEN status = 'disbanded' THEN 1 ELSE 0 END) as disbanded
130
+ FROM factions WHERE game_id = ?
131
+ `)
132
+ .get(gameId);
133
+ // Abilities
134
+ const abilityStats = db
135
+ .prepare(`
136
+ SELECT
137
+ SUM(CASE WHEN owner_type = 'template' THEN 1 ELSE 0 END) as templates,
138
+ SUM(CASE WHEN owner_type = 'character' THEN 1 ELSE 0 END) as character_owned
139
+ FROM abilities WHERE game_id = ?
140
+ `)
141
+ .get(gameId);
142
+ // Notes
143
+ const noteStats = db
144
+ .prepare(`
145
+ SELECT
146
+ COUNT(*) as total,
147
+ SUM(CASE WHEN pinned = 1 THEN 1 ELSE 0 END) as pinned
148
+ FROM notes WHERE game_id = ?
149
+ `)
150
+ .get(gameId);
151
+ // Status effects
152
+ const statusStats = db
153
+ .prepare(`
154
+ SELECT
155
+ COUNT(*) as count,
156
+ COUNT(DISTINCT target_id) as affected
157
+ FROM status_effects se
158
+ JOIN characters c ON c.id = se.target_id
159
+ WHERE c.game_id = ?
160
+ `)
161
+ .get(gameId);
162
+ // Tags
163
+ const tagCount = db
164
+ .prepare(`SELECT COUNT(DISTINCT tag) as count FROM tags WHERE game_id = ?`)
165
+ .get(gameId);
166
+ // Random tables
167
+ const tableCount = db
168
+ .prepare(`SELECT COUNT(*) as count FROM random_tables WHERE game_id = ?`)
169
+ .get(gameId);
170
+ // Narrative events
171
+ const narrativeStats = db
172
+ .prepare(`
173
+ SELECT
174
+ COUNT(*) as total,
175
+ SUM(CASE WHEN timestamp > datetime('now', '-1 hour') THEN 1 ELSE 0 END) as recent_hour
176
+ FROM narrative_events WHERE game_id = ?
177
+ `)
178
+ .get(gameId);
179
+ // Images
180
+ const imageCount = db
181
+ .prepare(`SELECT COUNT(*) as count FROM stored_images WHERE game_id = ?`)
182
+ .get(gameId);
183
+ // Calendar/Time
184
+ const calendarRow = db
185
+ .prepare(`SELECT * FROM game_time WHERE game_id = ?`)
186
+ .get(gameId);
187
+ // Get current location name
188
+ let playerLocation = null;
189
+ if (gameRow.current_location_id) {
190
+ const locRow = db
191
+ .prepare(`SELECT name FROM locations WHERE id = ?`)
192
+ .get(gameRow.current_location_id);
193
+ playerLocation = locRow?.name || null;
194
+ }
195
+ // Build the game state audit
196
+ const gameStateAudit = {
197
+ characters: {
198
+ total: characterStats.total || 0,
199
+ players: characterStats.players || 0,
200
+ npcs: characterStats.npcs || 0,
201
+ withNotes: characterStats.with_notes || 0,
202
+ withConditions: charactersWithConditions.count || 0,
203
+ },
204
+ locations: {
205
+ total: locationStats.total || 0,
206
+ connected: connectedLocations.count || 0,
207
+ },
208
+ quests: {
209
+ active: questStats.active || 0,
210
+ completed: questStats.completed || 0,
211
+ failed: questStats.failed || 0,
212
+ },
213
+ items: {
214
+ total: itemStats.total || 0,
215
+ inInventories: itemStats.in_inventories || 0,
216
+ inLocations: itemStats.in_locations || 0,
217
+ },
218
+ combat: {
219
+ active: !!combatRow,
220
+ combatId: combatRow?.id || null,
221
+ round: combatRow?.round || null,
222
+ participantCount: combatRow
223
+ ? (JSON.parse(combatRow.participants || "[]")).length
224
+ : 0,
225
+ },
226
+ resources: {
227
+ gameLevel: resourceStats.game_level || 0,
228
+ characterLevel: resourceStats.character_level || 0,
229
+ },
230
+ timers: {
231
+ active: timerStats.active || 0,
232
+ triggered: timerStats.triggered || 0,
233
+ },
234
+ scheduledEvents: {
235
+ pending: eventStats.pending || 0,
236
+ triggered: eventStats.triggered || 0,
237
+ },
238
+ relationships: {
239
+ total: relationshipCount.count || 0,
240
+ },
241
+ secrets: {
242
+ total: secretStats.total || 0,
243
+ revealed: secretStats.revealed || 0,
244
+ hidden: (secretStats.total || 0) - (secretStats.revealed || 0),
245
+ },
246
+ factions: {
247
+ active: factionStats.active || 0,
248
+ disbanded: factionStats.disbanded || 0,
249
+ },
250
+ abilities: {
251
+ templates: abilityStats.templates || 0,
252
+ characterOwned: abilityStats.character_owned || 0,
253
+ },
254
+ notes: {
255
+ total: noteStats.total || 0,
256
+ pinned: noteStats.pinned || 0,
257
+ },
258
+ statusEffects: {
259
+ activeCount: statusStats.count || 0,
260
+ affectedCharacters: statusStats.affected || 0,
261
+ },
262
+ tags: {
263
+ uniqueTags: tagCount.count || 0,
264
+ },
265
+ randomTables: {
266
+ total: tableCount.count || 0,
267
+ },
268
+ narrativeEvents: {
269
+ total: narrativeStats.total || 0,
270
+ recentHour: narrativeStats.recent_hour || 0,
271
+ },
272
+ images: {
273
+ total: imageCount.count || 0,
274
+ },
275
+ time: {
276
+ hasCalendar: !!calendarRow,
277
+ currentTime: calendarRow
278
+ ? JSON.stringify(safeJsonParse(calendarRow.current_time, null))
279
+ : null,
280
+ },
281
+ };
282
+ // ============================================================================
283
+ // PERSISTENCE REMINDERS
284
+ // ============================================================================
285
+ const persistenceReminders = [];
286
+ // CRITICAL: Active combat must be resolved or saved
287
+ if (gameStateAudit.combat.active && gameStateAudit.combat.combatId) {
288
+ persistenceReminders.push({
289
+ category: "critical",
290
+ entityType: "combat",
291
+ tool: "end_combat OR get_combat",
292
+ reminder: "ACTIVE COMBAT IN PROGRESS",
293
+ reason: `Combat is active (Round ${gameStateAudit.combat.round}). Either resolve it before pausing or ensure the full combat state is captured in your pause notes.`,
294
+ entityIds: [gameStateAudit.combat.combatId],
295
+ });
296
+ }
297
+ // CRITICAL: Status effects that may expire
298
+ if (gameStateAudit.statusEffects.activeCount > 0) {
299
+ persistenceReminders.push({
300
+ category: "critical",
301
+ entityType: "status_effects",
302
+ tool: "list_status_effects",
303
+ reminder: "Active status effects on characters",
304
+ reason: `${gameStateAudit.statusEffects.activeCount} status effect(s) on ${gameStateAudit.statusEffects.affectedCharacters} character(s). Note any that are narratively important.`,
305
+ });
306
+ }
307
+ // IMPORTANT: Recent narrative events should be logged
308
+ if (narrativeStats.recent_hour === 0) {
309
+ persistenceReminders.push({
310
+ category: "important",
311
+ entityType: "narrative_events",
312
+ tool: "log_event",
313
+ reminder: "No events logged in the last hour",
314
+ reason: "Consider logging key story moments before pausing. This helps with recaps and continuity.",
315
+ });
316
+ }
317
+ // IMPORTANT: Active timers need context
318
+ if (gameStateAudit.timers.active > 0) {
319
+ persistenceReminders.push({
320
+ category: "important",
321
+ entityType: "timers",
322
+ tool: "list_timers",
323
+ reminder: "Active timers running",
324
+ reason: `${gameStateAudit.timers.active} timer(s) are active. Document their narrative significance in your pause state.`,
325
+ });
326
+ }
327
+ // IMPORTANT: Pending scheduled events
328
+ if (gameStateAudit.scheduledEvents.pending > 0) {
329
+ persistenceReminders.push({
330
+ category: "important",
331
+ entityType: "scheduled_events",
332
+ tool: "list_scheduled_events",
333
+ reminder: "Pending scheduled events",
334
+ reason: `${gameStateAudit.scheduledEvents.pending} event(s) are scheduled. Note any that are imminent.`,
335
+ });
336
+ }
337
+ // IMPORTANT: Characters without notes
338
+ if (gameStateAudit.characters.npcs > 0 && gameStateAudit.characters.withNotes < gameStateAudit.characters.npcs) {
339
+ const npcsWithoutNotes = gameStateAudit.characters.npcs - gameStateAudit.characters.withNotes;
340
+ if (npcsWithoutNotes > 0) {
341
+ persistenceReminders.push({
342
+ category: "suggested",
343
+ entityType: "characters",
344
+ tool: "update_character",
345
+ reminder: "NPCs missing character notes",
346
+ reason: `${npcsWithoutNotes} NPC(s) have no notes. Consider updating notes for NPCs the player interacted with.`,
347
+ });
348
+ }
349
+ }
350
+ // SUGGESTED: Quest progress
351
+ if (gameStateAudit.quests.active > 0) {
352
+ persistenceReminders.push({
353
+ category: "suggested",
354
+ entityType: "quests",
355
+ tool: "complete_objective OR update_quest",
356
+ reminder: "Review active quest progress",
357
+ reason: `${gameStateAudit.quests.active} quest(s) active. Update objectives if any were completed or failed.`,
358
+ });
359
+ }
360
+ // SUGGESTED: Relationship changes
361
+ if (gameStateAudit.relationships.total > 0) {
362
+ persistenceReminders.push({
363
+ category: "suggested",
364
+ entityType: "relationships",
365
+ tool: "modify_relationship",
366
+ reminder: "Update relationship values if attitudes changed",
367
+ reason: `${gameStateAudit.relationships.total} relationship(s) tracked. Adjust values if NPCs' feelings toward the player changed.`,
368
+ });
369
+ }
370
+ // SUGGESTED: Secrets revealed or discovered
371
+ if (gameStateAudit.secrets.hidden > 0) {
372
+ persistenceReminders.push({
373
+ category: "suggested",
374
+ entityType: "secrets",
375
+ tool: "reveal_secret",
376
+ reminder: "Update secrets if any were discovered",
377
+ reason: `${gameStateAudit.secrets.hidden} secret(s) still hidden. Mark any as revealed if the player discovered them.`,
378
+ });
379
+ }
380
+ // SUGGESTED: Resources changed
381
+ if (gameStateAudit.resources.gameLevel + gameStateAudit.resources.characterLevel > 0) {
382
+ persistenceReminders.push({
383
+ category: "suggested",
384
+ entityType: "resources",
385
+ tool: "modify_resource",
386
+ reminder: "Update resources if any changed",
387
+ reason: "Review gold, reputation, or other tracked resources for changes during the game.",
388
+ });
389
+ }
390
+ // SUGGESTED: Items gained or lost
391
+ if (gameStateAudit.items.total > 0) {
392
+ persistenceReminders.push({
393
+ category: "suggested",
394
+ entityType: "items",
395
+ tool: "create_item OR transfer_item OR delete_item",
396
+ reminder: "Update inventory for items gained/lost",
397
+ reason: "Ensure any items picked up, dropped, or used are reflected in the game state.",
398
+ });
399
+ }
400
+ // SUGGESTED: Create a DM note for the game
401
+ persistenceReminders.push({
402
+ category: "suggested",
403
+ entityType: "notes",
404
+ tool: "create_note",
405
+ reminder: "Consider creating a game recap note",
406
+ reason: "A pinned recap note can help with continuity across sessions.",
407
+ });
408
+ // SUGGESTED: Update the in-game time
409
+ if (gameStateAudit.time.hasCalendar) {
410
+ persistenceReminders.push({
411
+ category: "suggested",
412
+ entityType: "time",
413
+ tool: "advance_time OR set_time",
414
+ reminder: "Update the in-game calendar",
415
+ reason: "Ensure the in-game time reflects how much time passed during this game.",
416
+ });
417
+ }
418
+ // Check for existing pause state
419
+ const existingPause = getPauseState(gameId);
420
+ // Build the ephemeral context checklist
421
+ const checklist = [
422
+ // Required items
423
+ {
424
+ category: "Scene Context",
425
+ item: "currentScene",
426
+ description: "Describe where we are in the story - what scene/location/moment is active",
427
+ required: true,
428
+ example: "The party is in the merchant's shop, having just discovered the hidden basement entrance",
429
+ },
430
+ {
431
+ category: "Scene Context",
432
+ item: "immediateSituation",
433
+ description: "What is happening RIGHT NOW - the exact moment we're pausing at",
434
+ required: true,
435
+ example: "Kira has her hand on the trapdoor handle, asking the party if they should descend",
436
+ },
437
+ // Important context
438
+ {
439
+ category: "Scene Context",
440
+ item: "sceneAtmosphere",
441
+ description: "Mood, lighting, ambient sounds, emotional tension level",
442
+ required: false,
443
+ example: "Tense and dusty, afternoon light through grimy windows, smell of old spices",
444
+ },
445
+ // Pending interactions
446
+ {
447
+ category: "Player Interaction",
448
+ item: "pendingPlayerAction",
449
+ description: "What action is the player considering or about to take?",
450
+ required: false,
451
+ example: "Player was deciding whether to open the trapdoor or search for traps first",
452
+ },
453
+ {
454
+ category: "Player Interaction",
455
+ item: "awaitingResponseTo",
456
+ description: "What question or prompt is awaiting the player's response?",
457
+ required: false,
458
+ example: "Asked player 'Do you want to descend first, or send someone else?'",
459
+ },
460
+ {
461
+ category: "Player Interaction",
462
+ item: "presentedChoices",
463
+ description: "Any formal choices that were presented to the player",
464
+ required: false,
465
+ },
466
+ // Narrative threads
467
+ {
468
+ category: "Narrative Threads",
469
+ item: "activeThreads",
470
+ description: "Ongoing storylines, investigations, or subplots that are 'in play'",
471
+ required: false,
472
+ example: "[{name: 'Missing Merchant', status: 'active', urgency: 'high'}]",
473
+ },
474
+ // DM plans
475
+ {
476
+ category: "DM Notes",
477
+ item: "dmShortTermPlans",
478
+ description: "What was about to happen next? Any planned encounters or reveals?",
479
+ required: false,
480
+ example: "If they descend, they'll find the merchant's body and trigger the ghost encounter",
481
+ },
482
+ {
483
+ category: "DM Notes",
484
+ item: "dmLongTermPlans",
485
+ description: "Major plot arcs being developed or built toward",
486
+ required: false,
487
+ example: "Building toward reveal that the merchant guild is a front for the cult",
488
+ },
489
+ {
490
+ category: "DM Notes",
491
+ item: "upcomingReveals",
492
+ description: "Secrets that are close to being discovered",
493
+ required: false,
494
+ },
495
+ // NPC state
496
+ {
497
+ category: "NPC Context",
498
+ item: "npcAttitudes",
499
+ description: "Current emotional states/attitudes of relevant NPCs (especially if shifted from baseline)",
500
+ required: false,
501
+ example: "{'guard_captain_id': 'suspicious of party after tavern incident'}",
502
+ },
503
+ {
504
+ category: "NPC Context",
505
+ item: "activeConversations",
506
+ description: "Any ongoing conversations and where they left off",
507
+ required: false,
508
+ },
509
+ // Player context
510
+ {
511
+ category: "Player Context",
512
+ item: "playerApparentGoals",
513
+ description: "What does the player seem to be trying to accomplish?",
514
+ required: false,
515
+ example: "Seems focused on finding the missing merchant, ignoring side hooks",
516
+ },
517
+ {
518
+ category: "Player Context",
519
+ item: "unresolvedHooks",
520
+ description: "Plot hooks the player noticed but hasn't pursued yet",
521
+ required: false,
522
+ example: "['The innkeeper's warning about the old mine', 'Strange lights in forest']",
523
+ },
524
+ // Tone
525
+ {
526
+ category: "Tone",
527
+ item: "recentTone",
528
+ description: "Recent narrative tone - was it tense? Comedic? Romantic? Action-heavy?",
529
+ required: false,
530
+ example: "Suspenseful mystery with moments of dark humor",
531
+ },
532
+ ];
533
+ // Build comprehensive instructions
534
+ const criticalCount = persistenceReminders.filter(r => r.category === "critical").length;
535
+ const importantCount = persistenceReminders.filter(r => r.category === "important").length;
536
+ const instructions = `
537
+ ═══════════════════════════════════════════════════════════════════════════════
538
+ PAUSE PREPARATION CHECKLIST
539
+ ═══════════════════════════════════════════════════════════════════════════════
540
+
541
+ This checklist helps you persist EVERYTHING before ending the game.
542
+
543
+ ${criticalCount > 0 ? `⚠️ ${criticalCount} CRITICAL item(s) require immediate attention!` : ""}
544
+ ${importantCount > 0 ? `📋 ${importantCount} IMPORTANT item(s) should be reviewed.` : ""}
545
+
546
+ ═══════════════════════════════════════════════════════════════════════════════
547
+ STEP 1: PERSIST GAME DATA
548
+ ═══════════════════════════════════════════════════════════════════════════════
549
+
550
+ Review the PERSISTENCE REMINDERS section. These are things that exist in the
551
+ database but may need updating based on what happened during play:
552
+
553
+ ${persistenceReminders.filter(r => r.category === "critical").map(r => `🔴 [CRITICAL] ${r.reminder}`).join("\n ") || "No critical items"}
554
+ ${persistenceReminders.filter(r => r.category === "important").map(r => `🟡 [IMPORTANT] ${r.reminder}`).join("\n ") || "No important items"}
555
+ ${persistenceReminders.filter(r => r.category === "suggested").map(r => `🟢 [SUGGESTED] ${r.reminder}`).join("\n ") || "No suggestions"}
556
+
557
+ ═══════════════════════════════════════════════════════════════════════════════
558
+ STEP 2: SAVE DM CONTEXT
559
+ ═══════════════════════════════════════════════════════════════════════════════
560
+
561
+ After persisting game data, call save_pause_state with your ephemeral context.
562
+ This captures what's in your "head" that ISN'T in the database:
563
+
564
+ REQUIRED:
565
+ • currentScene - Where we are in the story
566
+ • immediateSituation - What's happening RIGHT NOW (be specific!)
567
+
568
+ OPTIONAL (fill in what's relevant):
569
+ • Scene atmosphere, tone, pending player actions
570
+ • Active narrative threads and DM plans
571
+ • NPC attitudes and ongoing conversations
572
+ • Player goals and unresolved plot hooks
573
+
574
+ TIPS:
575
+ • Be specific about the EXACT moment - "hand on door handle" not "at the door"
576
+ • Capture emotional states and atmosphere for resumption
577
+ • Note what YOU were planning, not just what happened
578
+ • Write as if briefing a replacement DM mid-session
579
+
580
+ ═══════════════════════════════════════════════════════════════════════════════
581
+ STEP 3: VERIFY
582
+ ═══════════════════════════════════════════════════════════════════════════════
583
+
584
+ After saving, the game can be resumed using get_resume_context which will
585
+ provide the next DM (or you in a new context window) with everything needed
586
+ to continue seamlessly.
587
+
588
+ ═══════════════════════════════════════════════════════════════════════════════
589
+ `.trim();
590
+ return {
591
+ gameId,
592
+ gameName: gameRow.name,
593
+ currentState: {
594
+ playerLocation,
595
+ activeQuests: questStats.active || 0,
596
+ activeCombat: !!combatRow,
597
+ activeTimers: timerStats.active || 0,
598
+ pendingEvents: eventStats.pending || 0,
599
+ recentEventCount: narrativeStats.recent_hour || 0,
600
+ },
601
+ gameStateAudit,
602
+ persistenceReminders,
603
+ checklist,
604
+ existingPauseState: existingPause,
605
+ instructions,
606
+ };
607
+ }
608
+ export function savePauseState(params) {
609
+ const db = getDatabase();
610
+ const id = uuidv4();
611
+ const now = new Date().toISOString();
612
+ // Delete existing pause state if any (only one per session)
613
+ db.prepare(`DELETE FROM pause_states WHERE game_id = ?`).run(params.gameId);
614
+ const stmt = db.prepare(`
615
+ INSERT INTO pause_states (
616
+ id, game_id,
617
+ current_scene, scene_atmosphere, immediate_situation,
618
+ pending_player_action, awaiting_response_to, presented_choices,
619
+ active_threads,
620
+ dm_short_term_plans, dm_long_term_plans, upcoming_reveals,
621
+ npc_attitudes, active_conversations,
622
+ recent_tone, player_apparent_goals, unresolved_hooks,
623
+ pause_reason, created_at, model_used
624
+ )
625
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
626
+ `);
627
+ stmt.run(id, params.gameId, params.currentScene, params.sceneAtmosphere || null, params.immediateSituation, params.pendingPlayerAction || null, params.awaitingResponseTo || null, params.presentedChoices ? JSON.stringify(params.presentedChoices) : null, JSON.stringify(params.activeThreads || []), params.dmShortTermPlans || null, params.dmLongTermPlans || null, JSON.stringify(params.upcomingReveals || []), JSON.stringify(params.npcAttitudes || {}), JSON.stringify(params.activeConversations || []), params.recentTone || null, params.playerApparentGoals || null, JSON.stringify(params.unresolvedHooks || []), params.pauseReason || null, now, params.modelUsed || null);
628
+ // Also log a narrative event for the pause
629
+ const eventStmt = db.prepare(`
630
+ INSERT INTO narrative_events (id, game_id, event_type, content, metadata, timestamp)
631
+ VALUES (?, ?, ?, ?, ?, ?)
632
+ `);
633
+ eventStmt.run(uuidv4(), params.gameId, "game_paused", `Game paused: ${params.immediateSituation}`, JSON.stringify({
634
+ pauseReason: params.pauseReason,
635
+ modelUsed: params.modelUsed,
636
+ pauseStateId: id,
637
+ }), now);
638
+ return {
639
+ id,
640
+ gameId: params.gameId,
641
+ currentScene: params.currentScene,
642
+ sceneAtmosphere: params.sceneAtmosphere || null,
643
+ immediateSituation: params.immediateSituation,
644
+ pendingPlayerAction: params.pendingPlayerAction || null,
645
+ awaitingResponseTo: params.awaitingResponseTo || null,
646
+ presentedChoices: params.presentedChoices || null,
647
+ activeThreads: params.activeThreads || [],
648
+ dmShortTermPlans: params.dmShortTermPlans || null,
649
+ dmLongTermPlans: params.dmLongTermPlans || null,
650
+ upcomingReveals: params.upcomingReveals || [],
651
+ npcAttitudes: params.npcAttitudes || {},
652
+ activeConversations: params.activeConversations || [],
653
+ recentTone: params.recentTone || null,
654
+ playerApparentGoals: params.playerApparentGoals || null,
655
+ unresolvedHooks: params.unresolvedHooks || [],
656
+ pauseReason: params.pauseReason || null,
657
+ createdAt: now,
658
+ modelUsed: params.modelUsed || null,
659
+ };
660
+ }
661
+ // ============================================================================
662
+ // GET PAUSE STATE
663
+ // ============================================================================
664
+ export function getPauseState(gameId) {
665
+ const db = getDatabase();
666
+ const row = db
667
+ .prepare(`SELECT * FROM pause_states WHERE game_id = ?`)
668
+ .get(gameId);
669
+ if (!row)
670
+ return null;
671
+ return {
672
+ id: row.id,
673
+ gameId: row.game_id,
674
+ currentScene: row.current_scene,
675
+ sceneAtmosphere: row.scene_atmosphere,
676
+ immediateSituation: row.immediate_situation,
677
+ pendingPlayerAction: row.pending_player_action,
678
+ awaitingResponseTo: row.awaiting_response_to,
679
+ presentedChoices: row.presented_choices
680
+ ? JSON.parse(row.presented_choices)
681
+ : null,
682
+ activeThreads: JSON.parse(row.active_threads || "[]"),
683
+ dmShortTermPlans: row.dm_short_term_plans,
684
+ dmLongTermPlans: row.dm_long_term_plans,
685
+ upcomingReveals: JSON.parse(row.upcoming_reveals || "[]"),
686
+ npcAttitudes: JSON.parse(row.npc_attitudes || "{}"),
687
+ activeConversations: JSON.parse(row.active_conversations || "[]"),
688
+ recentTone: row.recent_tone,
689
+ playerApparentGoals: row.player_apparent_goals,
690
+ unresolvedHooks: JSON.parse(row.unresolved_hooks || "[]"),
691
+ pauseReason: row.pause_reason,
692
+ createdAt: row.created_at,
693
+ modelUsed: row.model_used,
694
+ };
695
+ }
696
+ // ============================================================================
697
+ // GET RESUME CONTEXT
698
+ // ============================================================================
699
+ /**
700
+ * Returns everything needed to resume a paused game seamlessly.
701
+ * Includes pause state, full game state, and a ready-to-use resume prompt.
702
+ */
703
+ export function getResumeContext(gameId) {
704
+ const db = getDatabase();
705
+ // Get pause state
706
+ const pauseState = getPauseState(gameId);
707
+ if (!pauseState)
708
+ return null;
709
+ // Get game
710
+ const gameRow = db
711
+ .prepare(`SELECT * FROM games WHERE id = ?`)
712
+ .get(gameId);
713
+ if (!gameRow)
714
+ return null;
715
+ const game = {
716
+ id: gameRow.id,
717
+ name: gameRow.name,
718
+ setting: gameRow.setting,
719
+ style: gameRow.style,
720
+ rules: gameRow.rules ? JSON.parse(gameRow.rules) : null,
721
+ preferences: gameRow.preferences
722
+ ? JSON.parse(gameRow.preferences)
723
+ : null,
724
+ currentLocationId: gameRow.current_location_id,
725
+ titleImageId: gameRow.title_image_id,
726
+ faviconImageId: gameRow.favicon_image_id,
727
+ createdAt: gameRow.created_at,
728
+ updatedAt: gameRow.updated_at,
729
+ };
730
+ // Get player character
731
+ const playerRow = db
732
+ .prepare(`SELECT * FROM characters WHERE game_id = ? AND is_player = 1 LIMIT 1`)
733
+ .get(gameId);
734
+ const playerCharacter = playerRow
735
+ ? {
736
+ id: playerRow.id,
737
+ gameId: playerRow.game_id,
738
+ name: playerRow.name,
739
+ isPlayer: true,
740
+ attributes: JSON.parse(playerRow.attributes || "{}"),
741
+ skills: JSON.parse(playerRow.skills || "{}"),
742
+ status: JSON.parse(playerRow.status || "{}"),
743
+ locationId: playerRow.location_id,
744
+ notes: playerRow.notes,
745
+ voice: playerRow.voice
746
+ ? JSON.parse(playerRow.voice)
747
+ : null,
748
+ imageGen: playerRow.image_gen
749
+ ? JSON.parse(playerRow.image_gen)
750
+ : null,
751
+ createdAt: playerRow.created_at,
752
+ }
753
+ : null;
754
+ // Get current location
755
+ const locationRow = game.currentLocationId
756
+ ? db
757
+ .prepare(`SELECT * FROM locations WHERE id = ?`)
758
+ .get(game.currentLocationId)
759
+ : null;
760
+ const currentLocation = locationRow
761
+ ? {
762
+ id: locationRow.id,
763
+ gameId: locationRow.game_id,
764
+ name: locationRow.name,
765
+ description: locationRow.description,
766
+ properties: JSON.parse(locationRow.properties || "{}"),
767
+ imageGen: locationRow.image_gen
768
+ ? JSON.parse(locationRow.image_gen)
769
+ : null,
770
+ }
771
+ : null;
772
+ // Get active quests
773
+ const questRows = db
774
+ .prepare(`SELECT * FROM quests WHERE game_id = ? AND status = 'active'`)
775
+ .all(gameId);
776
+ const activeQuests = questRows.map((row) => ({
777
+ id: row.id,
778
+ gameId: row.game_id,
779
+ name: row.name,
780
+ description: row.description,
781
+ objectives: JSON.parse(row.objectives || "[]"),
782
+ status: row.status,
783
+ rewards: row.rewards,
784
+ }));
785
+ // Get active combat
786
+ const combatRow = db
787
+ .prepare(`SELECT * FROM combats WHERE game_id = ? AND status = 'active' LIMIT 1`)
788
+ .get(gameId);
789
+ const activeCombat = combatRow
790
+ ? {
791
+ id: combatRow.id,
792
+ gameId: combatRow.game_id,
793
+ locationId: combatRow.location_id,
794
+ participants: JSON.parse(combatRow.participants || "[]"),
795
+ currentTurn: combatRow.current_turn,
796
+ round: combatRow.round,
797
+ status: combatRow.status,
798
+ log: JSON.parse(combatRow.log || "[]"),
799
+ }
800
+ : null;
801
+ // Get recent events (last 10)
802
+ const eventRows = db
803
+ .prepare(`SELECT * FROM narrative_events WHERE game_id = ? ORDER BY timestamp DESC LIMIT 10`)
804
+ .all(gameId);
805
+ const recentEvents = eventRows.map((row) => ({
806
+ id: row.id,
807
+ gameId: row.game_id,
808
+ eventType: row.event_type,
809
+ content: row.content,
810
+ metadata: JSON.parse(row.metadata || "{}"),
811
+ timestamp: row.timestamp,
812
+ }));
813
+ // Get active timers
814
+ const timerRows = db
815
+ .prepare(`SELECT * FROM timers WHERE game_id = ? AND triggered = 0`)
816
+ .all(gameId);
817
+ const activeTimers = timerRows.map((row) => ({
818
+ id: row.id,
819
+ gameId: row.game_id,
820
+ name: row.name,
821
+ description: row.description,
822
+ timerType: row.timer_type,
823
+ currentValue: row.current_value,
824
+ maxValue: row.max_value,
825
+ direction: row.direction,
826
+ triggerAt: row.trigger_at,
827
+ triggered: false,
828
+ unit: row.unit,
829
+ visibleToPlayers: Boolean(row.visible_to_players),
830
+ createdAt: row.created_at,
831
+ consequence: row.consequence ? safeJsonParse(row.consequence, null) : null,
832
+ }));
833
+ // Get pending scheduled events
834
+ const scheduledRows = db
835
+ .prepare(`SELECT * FROM scheduled_events WHERE game_id = ? AND triggered = 0`)
836
+ .all(gameId);
837
+ const pendingScheduledEvents = scheduledRows.map((row) => ({
838
+ id: row.id,
839
+ gameId: row.game_id,
840
+ name: row.name,
841
+ description: row.description,
842
+ triggerTime: JSON.parse(row.trigger_time),
843
+ recurring: row.recurring,
844
+ triggered: false,
845
+ metadata: JSON.parse(row.metadata || "{}"),
846
+ consequence: row.consequence ? safeJsonParse(row.consequence, null) : null,
847
+ }));
848
+ // Generate narrative summary from recent events
849
+ const narrativeSummary = recentEvents
850
+ .slice()
851
+ .reverse()
852
+ .map((e) => `[${e.eventType}] ${e.content}`)
853
+ .join("\n");
854
+ // Build resume prompt
855
+ const warnings = [];
856
+ if (activeCombat) {
857
+ warnings.push(`ACTIVE COMBAT: Round ${activeCombat.round}, waiting for turn ${activeCombat.currentTurn}`);
858
+ }
859
+ if (activeTimers.length > 0) {
860
+ warnings.push(`${activeTimers.length} active timer(s) - check if any are urgent`);
861
+ }
862
+ const pauseAge = Math.floor((Date.now() - new Date(pauseState.createdAt).getTime()) / 1000 / 60);
863
+ if (pauseAge > 60) {
864
+ warnings.push(`Pause state is ${Math.floor(pauseAge / 60)} hours old - player may need recap`);
865
+ }
866
+ const resumePrompt = `
867
+ ═══════════════════════════════════════════════════════════════════════════════
868
+ GAME RESUME BRIEFING
869
+ ═══════════════════════════════════════════════════════════════════════════════
870
+
871
+ GAME: ${game.name}
872
+ SETTING: ${game.setting} (${game.style})
873
+ PAUSED: ${pauseState.createdAt}${pauseState.modelUsed ? ` (by ${pauseState.modelUsed})` : ""}
874
+ ${pauseState.pauseReason ? `REASON: ${pauseState.pauseReason}` : ""}
875
+
876
+ ───────────────────────────────────────────────────────────────────────────────
877
+ CURRENT SCENE
878
+ ───────────────────────────────────────────────────────────────────────────────
879
+ ${pauseState.currentScene}
880
+
881
+ ATMOSPHERE: ${pauseState.sceneAtmosphere || "Not specified"}
882
+ TONE: ${pauseState.recentTone || "Not specified"}
883
+
884
+ ───────────────────────────────────────────────────────────────────────────────
885
+ IMMEDIATE SITUATION (Resume from here)
886
+ ───────────────────────────────────────────────────────────────────────────────
887
+ ${pauseState.immediateSituation}
888
+
889
+ ${pauseState.pendingPlayerAction ? `PLAYER WAS ABOUT TO: ${pauseState.pendingPlayerAction}` : ""}
890
+ ${pauseState.awaitingResponseTo ? `AWAITING RESPONSE TO: ${pauseState.awaitingResponseTo}` : ""}
891
+ ${pauseState.presentedChoices ? `CHOICES PRESENTED: ${pauseState.presentedChoices.join(" | ")}` : ""}
892
+
893
+ ───────────────────────────────────────────────────────────────────────────────
894
+ PLAYER CONTEXT
895
+ ───────────────────────────────────────────────────────────────────────────────
896
+ CHARACTER: ${playerCharacter?.name || "Unknown"}
897
+ LOCATION: ${currentLocation?.name || "Unknown"}
898
+ APPARENT GOALS: ${pauseState.playerApparentGoals || "Not noted"}
899
+
900
+ ${pauseState.unresolvedHooks.length > 0 ? `UNRESOLVED HOOKS:\n${pauseState.unresolvedHooks.map((h) => ` • ${h}`).join("\n")}` : ""}
901
+
902
+ ───────────────────────────────────────────────────────────────────────────────
903
+ ACTIVE QUESTS (${activeQuests.length})
904
+ ───────────────────────────────────────────────────────────────────────────────
905
+ ${activeQuests.map((q) => `• ${q.name}: ${q.description.substring(0, 80)}...`).join("\n") || "None"}
906
+
907
+ ───────────────────────────────────────────────────────────────────────────────
908
+ DM NOTES (Previous DM's Plans)
909
+ ───────────────────────────────────────────────────────────────────────────────
910
+ SHORT-TERM: ${pauseState.dmShortTermPlans || "None recorded"}
911
+ LONG-TERM: ${pauseState.dmLongTermPlans || "None recorded"}
912
+ ${pauseState.upcomingReveals.length > 0 ? `UPCOMING REVEALS:\n${pauseState.upcomingReveals.map((r) => ` • ${r}`).join("\n")}` : ""}
913
+
914
+ ───────────────────────────────────────────────────────────────────────────────
915
+ NARRATIVE THREADS
916
+ ───────────────────────────────────────────────────────────────────────────────
917
+ ${pauseState.activeThreads.map((t) => `• [${t.status.toUpperCase()}/${t.urgency}] ${t.name}: ${t.description}`).join("\n") || "None recorded"}
918
+
919
+ ${Object.keys(pauseState.npcAttitudes).length > 0 ? `───────────────────────────────────────────────────────────────────────────────
920
+ NPC ATTITUDES
921
+ ───────────────────────────────────────────────────────────────────────────────
922
+ ${Object.entries(pauseState.npcAttitudes).map(([id, attitude]) => `• ${id}: ${attitude}`).join("\n")}` : ""}
923
+
924
+ ${warnings.length > 0 ? `───────────────────────────────────────────────────────────────────────────────
925
+ ⚠️ WARNINGS
926
+ ───────────────────────────────────────────────────────────────────────────────
927
+ ${warnings.map((w) => `• ${w}`).join("\n")}` : ""}
928
+
929
+ ═══════════════════════════════════════════════════════════════════════════════
930
+ RESUME INSTRUCTIONS
931
+ ═══════════════════════════════════════════════════════════════════════════════
932
+
933
+ 1. Welcome the player back warmly
934
+ 2. Provide a brief "Previously..." recap if significant time has passed
935
+ 3. Re-establish the scene atmosphere
936
+ 4. Resume from the IMMEDIATE SITUATION - pick up exactly where we left off
937
+ 5. If choices were pending, re-present them naturally in the narrative
938
+
939
+ Remember: You're continuing mid-scene. Don't start fresh - pick up the thread!
940
+ ═══════════════════════════════════════════════════════════════════════════════
941
+ `.trim();
942
+ return {
943
+ pauseState,
944
+ gameState: {
945
+ game,
946
+ playerCharacter,
947
+ currentLocation,
948
+ activeQuests,
949
+ activeCombat,
950
+ recentEvents,
951
+ activeTimers,
952
+ pendingScheduledEvents,
953
+ },
954
+ narrativeSummary,
955
+ resumePrompt,
956
+ warnings,
957
+ };
958
+ }
959
+ /**
960
+ * Quick, lightweight context save for use during play.
961
+ * Agents should call this after significant moments.
962
+ * Updates only the most volatile fields without full pause ceremony.
963
+ */
964
+ export function saveContextSnapshot(params) {
965
+ const db = getDatabase();
966
+ // Check if pause state exists
967
+ const existing = getPauseState(params.gameId);
968
+ if (existing) {
969
+ // Update volatile fields only
970
+ const stmt = db.prepare(`
971
+ UPDATE pause_states SET
972
+ immediate_situation = ?,
973
+ player_apparent_goals = COALESCE(?, player_apparent_goals),
974
+ npc_attitudes = COALESCE(?, npc_attitudes),
975
+ created_at = ?
976
+ WHERE game_id = ?
977
+ `);
978
+ stmt.run(params.situation, params.playerIntent, params.npcMood ? JSON.stringify(params.npcMood) : null, new Date().toISOString(), params.gameId);
979
+ return {
980
+ success: true,
981
+ message: "Context snapshot updated",
982
+ };
983
+ }
984
+ else {
985
+ // Create minimal pause state
986
+ const id = uuidv4();
987
+ const now = new Date().toISOString();
988
+ const stmt = db.prepare(`
989
+ INSERT INTO pause_states (
990
+ id, game_id,
991
+ current_scene, immediate_situation,
992
+ player_apparent_goals, npc_attitudes,
993
+ active_threads, upcoming_reveals, active_conversations, unresolved_hooks,
994
+ created_at
995
+ )
996
+ VALUES (?, ?, ?, ?, ?, ?, '[]', '[]', '[]', '[]', ?)
997
+ `);
998
+ stmt.run(id, params.gameId, params.notes || "Snapshot taken during play", params.situation, params.playerIntent || null, params.npcMood ? JSON.stringify(params.npcMood) : "{}", now);
999
+ return {
1000
+ success: true,
1001
+ message: "Context snapshot created",
1002
+ suggestion: "Consider using prepare_pause and save_pause_state before ending the game for a complete save.",
1003
+ };
1004
+ }
1005
+ }
1006
+ // ============================================================================
1007
+ // DELETE PAUSE STATE
1008
+ // ============================================================================
1009
+ export function deletePauseState(gameId) {
1010
+ const db = getDatabase();
1011
+ const result = db
1012
+ .prepare(`DELETE FROM pause_states WHERE game_id = ?`)
1013
+ .run(gameId);
1014
+ return result.changes > 0;
1015
+ }
1016
+ // ============================================================================
1017
+ // CHECK IF CONTEXT NEEDS SAVE
1018
+ // ============================================================================
1019
+ /**
1020
+ * Returns a reminder if context hasn't been saved recently.
1021
+ * Use this to nudge agents to save context during long sessions.
1022
+ */
1023
+ export function checkContextFreshness(gameId) {
1024
+ const pauseState = getPauseState(gameId);
1025
+ if (!pauseState) {
1026
+ return {
1027
+ needsSave: true,
1028
+ lastSaved: null,
1029
+ minutesSinceLastSave: null,
1030
+ suggestion: "No context has been saved for this game. Consider calling save_context_snapshot to preserve current state.",
1031
+ };
1032
+ }
1033
+ const lastSaved = new Date(pauseState.createdAt);
1034
+ const minutesSince = Math.floor((Date.now() - lastSaved.getTime()) / 1000 / 60);
1035
+ if (minutesSince > 30) {
1036
+ return {
1037
+ needsSave: true,
1038
+ lastSaved: pauseState.createdAt,
1039
+ minutesSinceLastSave: minutesSince,
1040
+ suggestion: `Context is ${minutesSince} minutes old. Consider a quick save_context_snapshot to capture recent developments.`,
1041
+ };
1042
+ }
1043
+ return {
1044
+ needsSave: false,
1045
+ lastSaved: pauseState.createdAt,
1046
+ minutesSinceLastSave: minutesSince,
1047
+ suggestion: "Context is fresh - no immediate save needed.",
1048
+ };
1049
+ }
1050
+ /**
1051
+ * Push an update from an external agent (research agent, worldbuilder, etc.)
1052
+ * The primary DM agent will receive this update and can incorporate it.
1053
+ */
1054
+ export function pushExternalUpdate(params) {
1055
+ const db = getDatabase();
1056
+ const id = uuidv4();
1057
+ const now = new Date().toISOString();
1058
+ const stmt = db.prepare(`
1059
+ INSERT INTO external_updates (
1060
+ id, game_id,
1061
+ source_agent, source_description,
1062
+ update_type, category, title, content, structured_data,
1063
+ target_entity_id, target_entity_type,
1064
+ priority, status, created_at
1065
+ )
1066
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)
1067
+ `);
1068
+ stmt.run(id, params.gameId, params.sourceAgent, params.sourceDescription || null, params.updateType, params.category || null, params.title, params.content, params.structuredData ? JSON.stringify(params.structuredData) : null, params.targetEntityId || null, params.targetEntityType || null, params.priority || "normal", now);
1069
+ return {
1070
+ id,
1071
+ gameId: params.gameId,
1072
+ sourceAgent: params.sourceAgent,
1073
+ sourceDescription: params.sourceDescription || null,
1074
+ updateType: params.updateType,
1075
+ category: params.category || null,
1076
+ title: params.title,
1077
+ content: params.content,
1078
+ structuredData: params.structuredData || null,
1079
+ targetEntityId: params.targetEntityId || null,
1080
+ targetEntityType: params.targetEntityType || null,
1081
+ priority: params.priority || "normal",
1082
+ status: "pending",
1083
+ createdAt: now,
1084
+ acknowledgedAt: null,
1085
+ appliedAt: null,
1086
+ dmNotes: null,
1087
+ };
1088
+ }
1089
+ /**
1090
+ * Get all pending updates for a game.
1091
+ * Call this to check for new information from external agents.
1092
+ */
1093
+ export function getPendingUpdates(gameId) {
1094
+ const db = getDatabase();
1095
+ const rows = db
1096
+ .prepare(`SELECT * FROM external_updates
1097
+ WHERE game_id = ? AND status = 'pending'
1098
+ ORDER BY
1099
+ CASE priority
1100
+ WHEN 'urgent' THEN 1
1101
+ WHEN 'high' THEN 2
1102
+ WHEN 'normal' THEN 3
1103
+ WHEN 'low' THEN 4
1104
+ END,
1105
+ created_at DESC`)
1106
+ .all(gameId);
1107
+ const updates = rows.map((row) => ({
1108
+ id: row.id,
1109
+ gameId: row.game_id,
1110
+ sourceAgent: row.source_agent,
1111
+ sourceDescription: row.source_description,
1112
+ updateType: row.update_type,
1113
+ category: row.category,
1114
+ title: row.title,
1115
+ content: row.content,
1116
+ structuredData: row.structured_data
1117
+ ? JSON.parse(row.structured_data)
1118
+ : null,
1119
+ targetEntityId: row.target_entity_id,
1120
+ targetEntityType: row.target_entity_type,
1121
+ priority: row.priority,
1122
+ status: row.status,
1123
+ createdAt: row.created_at,
1124
+ acknowledgedAt: row.acknowledged_at,
1125
+ appliedAt: row.applied_at,
1126
+ dmNotes: row.dm_notes,
1127
+ }));
1128
+ const urgentCount = updates.filter((u) => u.priority === "urgent").length;
1129
+ let suggestion = "";
1130
+ if (urgentCount > 0) {
1131
+ suggestion = `⚠️ ${urgentCount} URGENT update(s) require immediate attention!`;
1132
+ }
1133
+ else if (updates.length > 0) {
1134
+ suggestion = `${updates.length} pending update(s) from external agents. Review and incorporate as appropriate.`;
1135
+ }
1136
+ else {
1137
+ suggestion = "No pending updates from external agents.";
1138
+ }
1139
+ return {
1140
+ gameId,
1141
+ pendingCount: updates.length,
1142
+ urgentCount,
1143
+ updates,
1144
+ hasUrgent: urgentCount > 0,
1145
+ suggestion,
1146
+ };
1147
+ }
1148
+ /**
1149
+ * Acknowledge an update (mark as seen by DM).
1150
+ */
1151
+ export function acknowledgeUpdate(updateId) {
1152
+ const db = getDatabase();
1153
+ const now = new Date().toISOString();
1154
+ db.prepare(`UPDATE external_updates SET status = 'acknowledged', acknowledged_at = ? WHERE id = ?`).run(now, updateId);
1155
+ return getExternalUpdate(updateId);
1156
+ }
1157
+ /**
1158
+ * Mark an update as applied (incorporated into the narrative).
1159
+ */
1160
+ export function applyUpdate(updateId, dmNotes) {
1161
+ const db = getDatabase();
1162
+ const now = new Date().toISOString();
1163
+ db.prepare(`UPDATE external_updates
1164
+ SET status = 'applied', applied_at = ?, dm_notes = COALESCE(?, dm_notes)
1165
+ WHERE id = ?`).run(now, dmNotes || null, updateId);
1166
+ return getExternalUpdate(updateId);
1167
+ }
1168
+ /**
1169
+ * Reject an update (not appropriate for the narrative).
1170
+ */
1171
+ export function rejectUpdate(updateId, dmNotes) {
1172
+ const db = getDatabase();
1173
+ db.prepare(`UPDATE external_updates
1174
+ SET status = 'rejected', dm_notes = COALESCE(?, dm_notes)
1175
+ WHERE id = ?`).run(dmNotes || null, updateId);
1176
+ return getExternalUpdate(updateId);
1177
+ }
1178
+ /**
1179
+ * Get a specific external update by ID.
1180
+ */
1181
+ export function getExternalUpdate(updateId) {
1182
+ const db = getDatabase();
1183
+ const row = db
1184
+ .prepare(`SELECT * FROM external_updates WHERE id = ?`)
1185
+ .get(updateId);
1186
+ if (!row)
1187
+ return null;
1188
+ return {
1189
+ id: row.id,
1190
+ gameId: row.game_id,
1191
+ sourceAgent: row.source_agent,
1192
+ sourceDescription: row.source_description,
1193
+ updateType: row.update_type,
1194
+ category: row.category,
1195
+ title: row.title,
1196
+ content: row.content,
1197
+ structuredData: row.structured_data
1198
+ ? JSON.parse(row.structured_data)
1199
+ : null,
1200
+ targetEntityId: row.target_entity_id,
1201
+ targetEntityType: row.target_entity_type,
1202
+ priority: row.priority,
1203
+ status: row.status,
1204
+ createdAt: row.created_at,
1205
+ acknowledgedAt: row.acknowledged_at,
1206
+ appliedAt: row.applied_at,
1207
+ dmNotes: row.dm_notes,
1208
+ };
1209
+ }
1210
+ /**
1211
+ * List all updates for a game with optional status filter.
1212
+ */
1213
+ export function listExternalUpdates(gameId, status) {
1214
+ const db = getDatabase();
1215
+ let query = `SELECT * FROM external_updates WHERE game_id = ?`;
1216
+ const params = [gameId];
1217
+ if (status) {
1218
+ query += ` AND status = ?`;
1219
+ params.push(status);
1220
+ }
1221
+ query += ` ORDER BY created_at DESC`;
1222
+ const rows = db.prepare(query).all(...params);
1223
+ return rows.map((row) => ({
1224
+ id: row.id,
1225
+ gameId: row.game_id,
1226
+ sourceAgent: row.source_agent,
1227
+ sourceDescription: row.source_description,
1228
+ updateType: row.update_type,
1229
+ category: row.category,
1230
+ title: row.title,
1231
+ content: row.content,
1232
+ structuredData: row.structured_data
1233
+ ? JSON.parse(row.structured_data)
1234
+ : null,
1235
+ targetEntityId: row.target_entity_id,
1236
+ targetEntityType: row.target_entity_type,
1237
+ priority: row.priority,
1238
+ status: row.status,
1239
+ createdAt: row.created_at,
1240
+ acknowledgedAt: row.acknowledged_at,
1241
+ appliedAt: row.applied_at,
1242
+ dmNotes: row.dm_notes,
1243
+ }));
1244
+ }
1245
+ /**
1246
+ * Delete an external update.
1247
+ */
1248
+ export function deleteExternalUpdate(updateId) {
1249
+ const db = getDatabase();
1250
+ const result = db
1251
+ .prepare(`DELETE FROM external_updates WHERE id = ?`)
1252
+ .run(updateId);
1253
+ return result.changes > 0;
1254
+ }