pi-harness-runtime 1.1.346 → 1.1.348

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 (125) hide show
  1. package/index.ts +71 -75
  2. package/package.json +1 -1
  3. package/packages/a2a-adapter/package.json +1 -1
  4. package/packages/agent-policy/package.json +1 -1
  5. package/packages/architecture-generator/package.json +1 -1
  6. package/packages/auth/package.json +1 -1
  7. package/packages/autonomous-refactor/package.json +1 -1
  8. package/packages/autonomous-runtime/package.json +1 -1
  9. package/packages/cache-strategy/package.json +1 -1
  10. package/packages/capability-registry/package.json +1 -1
  11. package/packages/checkpoint/package.json +1 -1
  12. package/packages/cli-plugin-sdk/package.json +1 -1
  13. package/packages/clipboard/package.json +1 -1
  14. package/packages/clipboard-plugin/package.json +1 -1
  15. package/packages/code-generation/package.json +1 -1
  16. package/packages/code-review/package.json +1 -1
  17. package/packages/codex-adapter/package.json +1 -1
  18. package/packages/codex-watcher/package.json +1 -1
  19. package/packages/config-capture/package.json +1 -1
  20. package/packages/connection-runtime/package.json +1 -1
  21. package/packages/context-compiler/package.json +1 -1
  22. package/packages/context-discovery/package.json +1 -1
  23. package/packages/context-manager/package.json +1 -1
  24. package/packages/contracts/package.json +1 -1
  25. package/packages/cookie-sanitizer/package.json +1 -1
  26. package/packages/cost-optimizer/package.json +1 -1
  27. package/packages/dependency-analyzer/package.json +1 -1
  28. package/packages/django-plugin/package.json +1 -1
  29. package/packages/doc-generator/package.json +1 -1
  30. package/packages/durable-review/package.json +1 -1
  31. package/packages/e2e-tester/package.json +1 -1
  32. package/packages/evaluation-engine/package.json +1 -1
  33. package/packages/evaluation-runner/package.json +1 -1
  34. package/packages/event-bus/package.json +1 -1
  35. package/packages/event-sink/package.json +1 -1
  36. package/packages/event-store/package.json +1 -1
  37. package/packages/experience-replay/package.json +1 -1
  38. package/packages/feedback-collector/package.json +1 -1
  39. package/packages/file-copy-helper/package.json +1 -1
  40. package/packages/framework-detector/package.json +1 -1
  41. package/packages/framework-plugin-sdk/package.json +1 -1
  42. package/packages/frappe-plugin/package.json +1 -1
  43. package/packages/generic-web-plugin/package.json +1 -1
  44. package/packages/health-monitor/package.json +1 -1
  45. package/packages/intent-analyzer/package.json +1 -1
  46. package/packages/jev-judge/package.json +1 -1
  47. package/packages/knowledge-graph/package.json +1 -1
  48. package/packages/knowledge-retrieval/package.json +1 -1
  49. package/packages/laravel-plugin/package.json +1 -1
  50. package/packages/learning-engine/package.json +1 -1
  51. package/packages/mcp-adapter/package.json +1 -1
  52. package/packages/memory-engine/package.json +1 -1
  53. package/packages/milestone-manager/package.json +1 -1
  54. package/packages/model-registry/package.json +1 -1
  55. package/packages/nextjs-plugin/package.json +1 -1
  56. package/packages/notification/dist/index.d.ts +1 -0
  57. package/packages/notification/dist/index.d.ts.map +1 -1
  58. package/packages/notification/dist/index.js +1 -0
  59. package/packages/notification/dist/index.js.map +1 -1
  60. package/packages/notification/dist/notification-center.d.ts +13 -0
  61. package/packages/notification/dist/notification-center.d.ts.map +1 -1
  62. package/packages/notification/dist/notification-center.js +91 -1
  63. package/packages/notification/dist/notification-center.js.map +1 -1
  64. package/packages/notification/dist/pending-question.d.ts +198 -0
  65. package/packages/notification/dist/pending-question.d.ts.map +1 -0
  66. package/packages/notification/dist/pending-question.js +410 -0
  67. package/packages/notification/dist/pending-question.js.map +1 -0
  68. package/packages/notification/package.json +1 -1
  69. package/packages/notification/src/index.ts +1 -0
  70. package/packages/notification/src/notification-center.ts +92 -1
  71. package/packages/notification/src/pending-question.ts +594 -0
  72. package/packages/notification/test/pending-question.test.ts +331 -0
  73. package/packages/observability/package.json +1 -1
  74. package/packages/okf-indexer/package.json +1 -1
  75. package/packages/paperclip-integration/package.json +1 -1
  76. package/packages/performance-optimizer/package.json +1 -1
  77. package/packages/permission-mode/package.json +1 -1
  78. package/packages/privilege-broker/package.json +1 -1
  79. package/packages/project-analyzer/package.json +1 -1
  80. package/packages/project-bootstrap/package.json +1 -1
  81. package/packages/projection-engine/package.json +1 -1
  82. package/packages/prompt-compiler/package.json +1 -1
  83. package/packages/prompt-versioning/package.json +1 -1
  84. package/packages/provider-adapter-sdk/package.json +1 -1
  85. package/packages/provider-router/package.json +1 -1
  86. package/packages/provider-selector/package.json +1 -1
  87. package/packages/providers/package.json +1 -1
  88. package/packages/qdrant-skills/package.json +1 -1
  89. package/packages/quota-manager/package.json +1 -1
  90. package/packages/rate-limiter/package.json +1 -1
  91. package/packages/react-vite-plugin/package.json +1 -1
  92. package/packages/relay/package.json +1 -1
  93. package/packages/release-manager/package.json +1 -1
  94. package/packages/requirement-compiler/package.json +1 -1
  95. package/packages/runtime/package.json +1 -1
  96. package/packages/scheduler/package.json +1 -1
  97. package/packages/scheduler-adapter/package.json +1 -1
  98. package/packages/session/package.json +1 -1
  99. package/packages/session-api/package.json +1 -1
  100. package/packages/session-export/package.json +1 -1
  101. package/packages/shared-context/package.json +1 -1
  102. package/packages/skill-mcp-client/package.json +1 -1
  103. package/packages/skill-registry/package.json +1 -1
  104. package/packages/skills/package.json +1 -1
  105. package/packages/sprint-planner/package.json +1 -1
  106. package/packages/subscription-engine/package.json +1 -1
  107. package/packages/task-compiler/package.json +1 -1
  108. package/packages/telegram/package.json +1 -1
  109. package/packages/tencentdb-memory/package.json +1 -1
  110. package/packages/tencentdb-sync/package.json +1 -1
  111. package/packages/test-data-generator/package.json +1 -1
  112. package/packages/test-generator/package.json +1 -1
  113. package/packages/todo-bd-sync/package.json +1 -1
  114. package/packages/token-estimation/package.json +1 -1
  115. package/packages/token-optimizer/package.json +1 -1
  116. package/packages/trajectory/package.json +1 -1
  117. package/packages/tui/package.json +1 -1
  118. package/packages/types/package.json +1 -1
  119. package/packages/voice-api/package.json +1 -1
  120. package/packages/webmcp-tester/package.json +1 -1
  121. package/packages/wiki/package.json +1 -1
  122. package/packages/workflow-events/package.json +1 -1
  123. package/packages/workspace-scanner/package.json +1 -1
  124. package/packages/worktree/package.json +1 -1
  125. package/packages/write-review/package.json +1 -1
@@ -0,0 +1,594 @@
1
+ /**
2
+ * Pending Question Controller
3
+ *
4
+ * Manages pending questions that require human response.
5
+ * Supports multiple channels (Telegram, LINE) and Jev evaluation.
6
+ */
7
+
8
+ import { logWarn } from "./file-logger.js";
9
+
10
+ /**
11
+ * Question kinds
12
+ */
13
+ export type QuestionKind =
14
+ | "routine" // "Should I continue?"
15
+ | "approval" // "Want me to implement X?"
16
+ | "clarification" // "Do you mean X or Y?"
17
+ | "explicit"; // Explicit approval required
18
+
19
+ /**
20
+ * Question states
21
+ */
22
+ export type QuestionState =
23
+ | "pending" // Waiting for response
24
+ | "evaluating" // Jev is evaluating
25
+ | "resolved" // User answered
26
+ | "cancelled" // Cancelled (e.g., session switch)
27
+ | "expired"; // Timed out
28
+
29
+ /**
30
+ * Channel delivery states
31
+ */
32
+ export interface ChannelDeliveryState {
33
+ delivered: boolean;
34
+ deliveredAt?: Date;
35
+ error?: string;
36
+ }
37
+
38
+ /**
39
+ * Pending Question
40
+ */
41
+ export interface PendingQuestion {
42
+ /** Unique request ID for this question */
43
+ requestId: string;
44
+ /** Session ID */
45
+ sessionId: string;
46
+ /** Workspace path */
47
+ workspacePath: string;
48
+ /** Job ID if associated */
49
+ jobId?: string;
50
+ /** The question text */
51
+ questionText: string;
52
+ /** Question kind */
53
+ kind: QuestionKind;
54
+ /** Current state */
55
+ state: QuestionState;
56
+ /** When created */
57
+ createdAt: Date;
58
+ /** When evaluation is due (for Jev) */
59
+ dueAt?: Date;
60
+ /** When resolved */
61
+ resolvedAt?: Date;
62
+ /** Resolution: "yes", "no", "clarification", or undefined */
63
+ resolution?: "yes" | "no" | "clarification";
64
+ /** Who resolved it */
65
+ resolvedBy?: "user" | "jev" | "timeout";
66
+ /** Channel delivery states */
67
+ deliveryState: {
68
+ telegram?: ChannelDeliveryState;
69
+ line?: ChannelDeliveryState;
70
+ };
71
+ /** Whether user was notified */
72
+ userNotified: boolean;
73
+ /** For deduplication: last notified question text */
74
+ lastNotifiedQuestion?: string;
75
+ /** State revision for deduplication */
76
+ revision: number;
77
+ }
78
+
79
+ /**
80
+ * Question detection result
81
+ */
82
+ export interface DetectionResult {
83
+ detected: boolean;
84
+ questionText?: string;
85
+ kind?: QuestionKind;
86
+ isRoutine?: boolean;
87
+ }
88
+
89
+ /**
90
+ * Jev evaluation result
91
+ */
92
+ export interface JevDecision {
93
+ action: "continue" | "wait" | "clarify" | "stop";
94
+ reason: string;
95
+ eligible: boolean;
96
+ metadata?: Record<string, unknown>;
97
+ }
98
+
99
+ /**
100
+ * Pending Question Controller
101
+ */
102
+ export class PendingQuestionController {
103
+ private questions: Map<string, PendingQuestion> = new Map();
104
+ private currentQuestion: PendingQuestion | null = null;
105
+ private jevDecisionCallback?: (question: PendingQuestion, decision: JevDecision) => void;
106
+ private userAnswerCallback?: (question: PendingQuestion, answer: string) => void;
107
+ private config: {
108
+ evaluationDelayMs: number;
109
+ jevEnabled: boolean;
110
+ };
111
+
112
+ constructor(config?: {
113
+ evaluationDelayMs?: number;
114
+ jevEnabled?: boolean;
115
+ }) {
116
+ this.config = {
117
+ evaluationDelayMs: config?.evaluationDelayMs ?? 5 * 60 * 1000, // 5 minutes
118
+ jevEnabled: config?.jevEnabled ?? true,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Set callback for Jev decisions
124
+ */
125
+ onJevDecision(callback: (question: PendingQuestion, decision: JevDecision) => void): void {
126
+ this.jevDecisionCallback = callback;
127
+ }
128
+
129
+ /**
130
+ * Set callback for user answers
131
+ */
132
+ onUserAnswer(callback: (question: PendingQuestion, answer: string) => void): void {
133
+ this.userAnswerCallback = callback;
134
+ }
135
+
136
+ /**
137
+ * Generate unique request ID
138
+ */
139
+ private generateRequestId(): string {
140
+ return `q-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
141
+ }
142
+
143
+ /**
144
+ * Detect if text contains a question requiring response
145
+ */
146
+ detectQuestion(text: string, _options?: {
147
+ sessionId?: string;
148
+ workspacePath?: string;
149
+ jobId?: string;
150
+ }
151
+ ): DetectionResult {
152
+ // Find all question positions and extract each question sentence
153
+ const questionPositions: Array<{ start: number; end: number; sentence: string }> = [];
154
+ let searchFrom = 0;
155
+
156
+ while (true) {
157
+ const questionIndex = text.indexOf('?', searchFrom);
158
+ if (questionIndex === -1) break;
159
+
160
+ // Check if this question is inside quotes - if so, skip
161
+ const beforeQuestion = text.substring(0, questionIndex);
162
+
163
+ // Count unescaped quotes before and after
164
+ const openQuotes = (beforeQuestion.match(/"/g) || []).length;
165
+ const isInsideQuotes = openQuotes % 2 === 1;
166
+
167
+ if (isInsideQuotes) {
168
+ // This question is inside quotes - skip it
169
+ searchFrom = questionIndex + 1;
170
+ continue;
171
+ }
172
+
173
+ // Extract sentence for this question
174
+ let sentenceStart = searchFrom;
175
+
176
+ // Look backwards for sentence start
177
+ for (let i = questionIndex - 1; i >= searchFrom; i--) {
178
+ const char = text[i];
179
+ if (char === '.' || char === '!' || char === '\n') {
180
+ sentenceStart = i + 1;
181
+ break;
182
+ }
183
+ }
184
+
185
+ const sentence = text.substring(sentenceStart, questionIndex + 1).trim();
186
+ questionPositions.push({ start: sentenceStart, end: questionIndex, sentence });
187
+ searchFrom = questionIndex + 1;
188
+ }
189
+
190
+ // Check each question for approval/clarification first (priority)
191
+ for (const q of questionPositions) {
192
+ // Skip quoted questions
193
+ if (this.isQuotedOrCode(text, q.sentence)) {
194
+ continue;
195
+ }
196
+
197
+ const isRoutine = this.isRoutineContinuation(q.sentence);
198
+ const kind = this.determineKind(q.sentence, isRoutine);
199
+
200
+ // Return first approval or clarification question found
201
+ if (kind === "approval" || kind === "clarification") {
202
+ return {
203
+ detected: true,
204
+ questionText: q.sentence,
205
+ kind,
206
+ isRoutine,
207
+ };
208
+ }
209
+ }
210
+
211
+ // If no approval/clarification found, return first non-quoted routine question
212
+ for (const q of questionPositions) {
213
+ if (this.isQuotedOrCode(text, q.sentence)) {
214
+ continue;
215
+ }
216
+
217
+ const isRoutine = this.isRoutineContinuation(q.sentence);
218
+
219
+ return {
220
+ detected: true,
221
+ questionText: q.sentence,
222
+ kind: "routine",
223
+ isRoutine,
224
+ };
225
+ }
226
+
227
+ // No question found - check for approval patterns without ?
228
+ const approvalResult = this.checkApprovalPatterns(text);
229
+ if (approvalResult) return approvalResult;
230
+
231
+ return { detected: false };
232
+ }
233
+
234
+ /**
235
+ * Check for approval patterns in text without question mark
236
+ * Only triggers if the text is short and looks like a question (no example: prefix)
237
+ */
238
+ private checkApprovalPatterns(text: string): DetectionResult | null {
239
+ // Don't trigger on example/quotation text or code
240
+ const lower = text.toLowerCase();
241
+ if (/\b(example|such as|like this|for instance)\b/.test(lower)) {
242
+ return null;
243
+ }
244
+
245
+ // Don't trigger on code blocks
246
+ if (/```/.test(text)) {
247
+ return null;
248
+ }
249
+
250
+ // Only trigger if text is reasonably short (likely a real question, not a paragraph)
251
+ if (text.length > 200) {
252
+ return null;
253
+ }
254
+
255
+ const approvalPatterns = [
256
+ /\bwant me to\b/i,
257
+ /\bwould you like me to\b/i,
258
+ /\bshall i\b/i,
259
+ /\bshould i add\b/i,
260
+ /\bshould i implement\b/i,
261
+ /\bshould we add\b/i,
262
+ /\bshould we implement\b/i,
263
+ /\bdo you want me to\b/i,
264
+ /\bcan i (start|proceed|continue)\b/i,
265
+ ];
266
+
267
+ for (const pattern of approvalPatterns) {
268
+ if (pattern.test(text)) {
269
+ return {
270
+ detected: true,
271
+ questionText: text.trim(),
272
+ kind: "approval",
273
+ isRoutine: false,
274
+ };
275
+ }
276
+ }
277
+
278
+ return null;
279
+ }
280
+
281
+ /**
282
+ * Check if question is quoted or in code block
283
+ */
284
+ private isQuotedOrCode(fullText: string, question: string): boolean {
285
+ // Normalize question for comparison - remove surrounding quotes if present
286
+ let normalizedQuestion = question.trim();
287
+
288
+ // Check if the question itself starts with a quote
289
+ const startsWithQuote = normalizedQuestion.startsWith('"') || normalizedQuestion.startsWith("'");
290
+ if (startsWithQuote) {
291
+ return true;
292
+ }
293
+
294
+ // Check if question appears inside code block
295
+ const codeBlockPattern = /```[\s\S]*?```/g;
296
+ const codeBlocks = fullText.match(codeBlockPattern) || [];
297
+ for (const block of codeBlocks) {
298
+ // Check if the normalized question is inside this code block
299
+ if (block.includes(normalizedQuestion)) {
300
+ return true;
301
+ }
302
+ }
303
+
304
+ // Check if the question text appears inside quotes in the original full text
305
+ // This catches cases like: Here's an example: "Want me to implement this?"
306
+ // where the extracted sentence is "Here's an example: "Want me to implement this?"
307
+ // but we need to check if the CORE question is inside quotes
308
+ const questionWithoutQ = normalizedQuestion.replace(/\?$/, '');
309
+
310
+ // Look for the question text inside double quotes in the original text
311
+ // Pattern: find "...question...?" or "...question..."
312
+ const escapedQuestion = questionWithoutQ.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
313
+ const quotedQuestionPattern = new RegExp(`"[^"]*${escapedQuestion}[^"]*"`, 'i');
314
+ if (quotedQuestionPattern.test(fullText)) {
315
+ return true;
316
+ }
317
+
318
+ // Also check single quotes
319
+ const singleQuotedPattern = new RegExp(`'[^']*${escapedQuestion}[^']*'`, 'i');
320
+ if (singleQuotedPattern.test(fullText)) {
321
+ return true;
322
+ }
323
+
324
+ return false;
325
+ }
326
+
327
+ /**
328
+ * Determine question kind
329
+ */
330
+ private determineKind(question: string, isRoutine: boolean): QuestionKind {
331
+ if (isRoutine) return "routine";
332
+
333
+ const lower = question.toLowerCase();
334
+
335
+ // Clarification patterns (check first - more specific)
336
+ const clarifyPatterns = [
337
+ /\bdo you mean\b/i,
338
+ /\bwhich (one|option|approach)\b/i,
339
+ /\bshould i use\b/i,
340
+ /\bprefers?\b/i,
341
+ /\bwhich (one|option)\b/i,
342
+ ];
343
+
344
+ for (const pattern of clarifyPatterns) {
345
+ if (pattern.test(lower)) {
346
+ return "clarification";
347
+ }
348
+ }
349
+
350
+ // Explicit approval patterns (want me to, would you like me to, etc.)
351
+ const approvalPatterns = [
352
+ /\bwant me to\b/i,
353
+ /\bwould you like me to\b/i,
354
+ /\bshall i\b/i,
355
+ /\bshould i add\b/i,
356
+ /\bshould i implement\b/i,
357
+ /\bshould we add\b/i,
358
+ /\bshould we implement\b/i,
359
+ /\bdo you want me to\b/i,
360
+ /\bcan i (start|proceed|add|implement)\b/i,
361
+ ];
362
+
363
+ for (const pattern of approvalPatterns) {
364
+ if (pattern.test(lower)) {
365
+ return "approval";
366
+ }
367
+ }
368
+
369
+ return "routine";
370
+ }
371
+
372
+ /**
373
+ * Check if it's a routine continuation question
374
+ */
375
+ private isRoutineContinuation(question: string): boolean {
376
+ const lower = question.toLowerCase();
377
+
378
+ // Be specific - these patterns mean "continue with next task"
379
+ // Don't match approval patterns like "Would you like me to add X next?"
380
+ const routinePatterns = [
381
+ /\bshould i continue/i,
382
+ /\bshould we continue/i,
383
+ /\bcan i continue/i,
384
+ /\bcan we continue/i,
385
+ /\bmay i continue/i,
386
+ /\bshall i proceed/i,
387
+ /\bshould i proceed/i,
388
+ /^continue\?$/i,
389
+ /^proceed\?$/i,
390
+ /^next\?$/i,
391
+ /\bshall we (proceed|continue)/i,
392
+ /\bmay we (proceed|continue)/i,
393
+ /\bcan i proceed/i,
394
+ /\bshould i continue with/i,
395
+ /\bcan i proceed with/i,
396
+ ];
397
+
398
+ for (const pattern of routinePatterns) {
399
+ if (pattern.test(lower)) {
400
+ return true;
401
+ }
402
+ }
403
+
404
+ return false;
405
+ }
406
+
407
+ /**
408
+ * Create a new pending question
409
+ */
410
+ createQuestion(params: {
411
+ text: string;
412
+ sessionId: string;
413
+ workspacePath: string;
414
+ jobId?: string;
415
+ kind: QuestionKind;
416
+ }): PendingQuestion {
417
+ const requestId = this.generateRequestId();
418
+
419
+ const question: PendingQuestion = {
420
+ requestId,
421
+ sessionId: params.sessionId,
422
+ workspacePath: params.workspacePath,
423
+ jobId: params.jobId,
424
+ questionText: params.text,
425
+ kind: params.kind,
426
+ state: "pending",
427
+ createdAt: new Date(),
428
+ dueAt: new Date(Date.now() + this.config.evaluationDelayMs),
429
+ deliveryState: {},
430
+ userNotified: false,
431
+ revision: 1,
432
+ };
433
+
434
+ this.questions.set(requestId, question);
435
+ this.currentQuestion = question;
436
+
437
+ logWarn("[PendingQuestion]", `Created question ${requestId}: ${params.text.slice(0, 50)}...`);
438
+
439
+ return question;
440
+ }
441
+
442
+ /**
443
+ * Get current pending question
444
+ */
445
+ getCurrentQuestion(): PendingQuestion | null {
446
+ return this.currentQuestion;
447
+ }
448
+
449
+ /**
450
+ * Check if there's a pending question
451
+ */
452
+ hasPendingQuestion(): boolean {
453
+ return this.currentQuestion !== null &&
454
+ this.currentQuestion.state === "pending";
455
+ }
456
+
457
+ /**
458
+ * Update delivery state for a channel
459
+ */
460
+ updateDeliveryState(
461
+ channel: "telegram" | "line",
462
+ delivered: boolean,
463
+ error?: string
464
+ ): void {
465
+ if (!this.currentQuestion) return;
466
+
467
+ this.currentQuestion.deliveryState[channel] = {
468
+ delivered,
469
+ deliveredAt: delivered ? new Date() : undefined,
470
+ error,
471
+ };
472
+
473
+ if (delivered) {
474
+ this.currentQuestion.userNotified = true;
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Resolve the current question
480
+ */
481
+ resolve(params: {
482
+ resolution: "yes" | "no" | "clarification";
483
+ by: "user" | "jev" | "timeout";
484
+ }): void {
485
+ if (!this.currentQuestion) {
486
+ logWarn("[PendingQuestion]", "No question to resolve");
487
+ return;
488
+ }
489
+
490
+ this.currentQuestion.state = "resolved";
491
+ this.currentQuestion.resolution = params.resolution;
492
+ this.currentQuestion.resolvedBy = params.by;
493
+ this.currentQuestion.resolvedAt = new Date();
494
+
495
+ logWarn("[PendingQuestion]", `Resolved question ${this.currentQuestion.requestId}: ${params.resolution} by ${params.by}`);
496
+
497
+ // Notify callback
498
+ if (this.userAnswerCallback) {
499
+ this.userAnswerCallback(this.currentQuestion, params.resolution);
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Cancel the current question (e.g., on session switch)
505
+ */
506
+ cancel(reason: string = "cancelled"): void {
507
+ if (!this.currentQuestion) return;
508
+
509
+ this.currentQuestion.state = "cancelled";
510
+ logWarn("[PendingQuestion]", `Cancelled question ${this.currentQuestion.requestId}: ${reason}`);
511
+
512
+ this.currentQuestion = null;
513
+ }
514
+
515
+ /**
516
+ * Start Jev evaluation
517
+ */
518
+ startEvaluation(): void {
519
+ if (!this.currentQuestion) return;
520
+
521
+ if (this.currentQuestion.state !== "pending") {
522
+ logWarn("[PendingQuestion]", `Cannot start evaluation: state is ${this.currentQuestion.state}`);
523
+ return;
524
+ }
525
+
526
+ this.currentQuestion.state = "evaluating";
527
+ logWarn("[PendingQuestion]", `Started evaluation for question ${this.currentQuestion.requestId}`);
528
+ }
529
+
530
+ /**
531
+ * Get pending question by request ID
532
+ */
533
+ getQuestion(requestId: string): PendingQuestion | undefined {
534
+ return this.questions.get(requestId);
535
+ }
536
+
537
+ /**
538
+ * Get all questions
539
+ */
540
+ getAllQuestions(): PendingQuestion[] {
541
+ return Array.from(this.questions.values());
542
+ }
543
+
544
+ /**
545
+ * Clean up old resolved questions
546
+ */
547
+ cleanup(maxAgeMs: number = 60 * 60 * 1000): void {
548
+ const now = Date.now();
549
+ for (const [id, q] of this.questions.entries()) {
550
+ if (q.resolvedAt && (now - q.resolvedAt.getTime()) > maxAgeMs) {
551
+ this.questions.delete(id);
552
+ }
553
+ }
554
+ }
555
+
556
+ /**
557
+ * Check if question is answered
558
+ */
559
+ isResolved(): boolean {
560
+ return this.currentQuestion?.state === "resolved";
561
+ }
562
+
563
+ /**
564
+ * Get resolution
565
+ */
566
+ getResolution(): PendingQuestion["resolution"] | undefined {
567
+ return this.currentQuestion?.resolution;
568
+ }
569
+
570
+ /**
571
+ * Check if should suppress completion
572
+ */
573
+ shouldSuppressCompletion(): boolean {
574
+ return this.hasPendingQuestion();
575
+ }
576
+ }
577
+
578
+ /** Singleton instance */
579
+ let instance: PendingQuestionController | null = null;
580
+
581
+ export function getPendingQuestionController(): PendingQuestionController {
582
+ if (!instance) {
583
+ instance = new PendingQuestionController();
584
+ }
585
+ return instance;
586
+ }
587
+
588
+ export function createPendingQuestionController(config?: {
589
+ evaluationDelayMs?: number;
590
+ jevEnabled?: boolean;
591
+ }): PendingQuestionController {
592
+ instance = new PendingQuestionController(config);
593
+ return instance;
594
+ }