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.
- package/index.ts +71 -75
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/dist/index.d.ts +1 -0
- package/packages/notification/dist/index.d.ts.map +1 -1
- package/packages/notification/dist/index.js +1 -0
- package/packages/notification/dist/index.js.map +1 -1
- package/packages/notification/dist/notification-center.d.ts +13 -0
- package/packages/notification/dist/notification-center.d.ts.map +1 -1
- package/packages/notification/dist/notification-center.js +91 -1
- package/packages/notification/dist/notification-center.js.map +1 -1
- package/packages/notification/dist/pending-question.d.ts +198 -0
- package/packages/notification/dist/pending-question.d.ts.map +1 -0
- package/packages/notification/dist/pending-question.js +410 -0
- package/packages/notification/dist/pending-question.js.map +1 -0
- package/packages/notification/package.json +1 -1
- package/packages/notification/src/index.ts +1 -0
- package/packages/notification/src/notification-center.ts +92 -1
- package/packages/notification/src/pending-question.ts +594 -0
- package/packages/notification/test/pending-question.test.ts +331 -0
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- 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
|
+
}
|