happy-imou-cloud 2.0.20 → 2.0.22

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 (35) hide show
  1. package/bin/happy-cloud.mjs +1 -1
  2. package/dist/{BaseReasoningProcessor-B9z785Pi.cjs → BaseReasoningProcessor-CJVv1aNR.cjs} +3 -3
  3. package/dist/{BaseReasoningProcessor-iTk5q5Vy.mjs → BaseReasoningProcessor-mIqqngd3.mjs} +3 -3
  4. package/dist/ProviderSelectionHandler-BjLyIfSR.mjs +673 -0
  5. package/dist/ProviderSelectionHandler-e4zL4Y5_.cjs +680 -0
  6. package/dist/{api-BKnzORe4.cjs → api-DP-RQUao.cjs} +88 -3
  7. package/dist/{api-_Y2kzqZL.mjs → api-DrijKeDb.mjs} +86 -4
  8. package/dist/{command-BDkgHdQU.mjs → command--vV6BSsL.mjs} +3 -3
  9. package/dist/{command-BBNzMWEG.cjs → command-BZphfJrt.cjs} +3 -3
  10. package/dist/{index-Bg4KLXYY.mjs → index-BIki80pQ.mjs} +8 -8
  11. package/dist/{index-0TIyXMQu.cjs → index-CqCEZDFi.cjs} +11 -11
  12. package/dist/index.cjs +3 -3
  13. package/dist/index.mjs +3 -3
  14. package/dist/lib.cjs +1 -1
  15. package/dist/lib.d.cts +393 -36
  16. package/dist/lib.d.mts +393 -36
  17. package/dist/lib.mjs +1 -1
  18. package/dist/{persistence-BCkHc68b.mjs → persistence-C3NBdZdz.mjs} +1 -1
  19. package/dist/{persistence-D5uolhHo.cjs → persistence-yVTbf_Ng.cjs} +1 -1
  20. package/dist/{registerKillSessionHandler-DJMH-gar.mjs → registerKillSessionHandler-CHEj7UjN.mjs} +3 -3
  21. package/dist/{registerKillSessionHandler-BMUE5Oaj.cjs → registerKillSessionHandler-QmBN446A.cjs} +3 -3
  22. package/dist/{runClaude-BUhD2jR2.cjs → runClaude-BuI6OOEv.cjs} +72 -9
  23. package/dist/{runClaude-BMv-eao6.mjs → runClaude-D0DD_Ya5.mjs} +72 -9
  24. package/dist/{runCodex-DyNSDN6X.cjs → runCodex-1jTTmCvq.cjs} +84 -14
  25. package/dist/{runCodex-DwnLnXWK.mjs → runCodex-BzZ0jODI.mjs} +84 -14
  26. package/dist/{runGemini-UKpRbyNY.cjs → runGemini-1gJRE8oT.cjs} +5 -5
  27. package/dist/{runGemini-uVHDcJ09.mjs → runGemini-Bx2SYAyG.mjs} +5 -5
  28. package/package.json +1 -1
  29. package/scripts/build.mjs +66 -66
  30. package/scripts/devtools/README.md +9 -9
  31. package/scripts/e2e/fake-codex-acp-agent.mjs +139 -139
  32. package/scripts/e2e/local-server-session-roundtrip.mjs +1063 -1063
  33. package/scripts/release-smoke.mjs +3 -3
  34. package/dist/ProviderSelectionHandler-DUBEXkmo.cjs +0 -265
  35. package/dist/ProviderSelectionHandler-s79sTquD.mjs +0 -261
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { execFileSync } from 'child_process';
4
4
  import { fileURLToPath } from 'url';
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-0TIyXMQu.cjs');
4
- var api = require('./api-BKnzORe4.cjs');
5
- var registerKillSessionHandler = require('./registerKillSessionHandler-BMUE5Oaj.cjs');
3
+ var index = require('./index-CqCEZDFi.cjs');
4
+ var api = require('./api-DP-RQUao.cjs');
5
+ var registerKillSessionHandler = require('./registerKillSessionHandler-QmBN446A.cjs');
6
6
  var node_events = require('node:events');
7
7
  var node_crypto = require('node:crypto');
8
8
 
@@ -1,6 +1,6 @@
1
- import { p as publishSessionRegistration } from './index-Bg4KLXYY.mjs';
2
- import { s as startOfflineReconnection, c as configuration, i as isAuthenticationRequiredError, l as logger } from './api-_Y2kzqZL.mjs';
3
- import { c as createSessionMetadata } from './registerKillSessionHandler-DJMH-gar.mjs';
1
+ import { p as publishSessionRegistration } from './index-BIki80pQ.mjs';
2
+ import { s as startOfflineReconnection, c as configuration, i as isAuthenticationRequiredError, l as logger } from './api-DrijKeDb.mjs';
3
+ import { c as createSessionMetadata } from './registerKillSessionHandler-CHEj7UjN.mjs';
4
4
  import { EventEmitter } from 'node:events';
5
5
  import { randomUUID } from 'node:crypto';
6
6
 
@@ -0,0 +1,673 @@
1
+ import { H as HAPPY_ORG_REPEAT_THRESHOLD, b as HAPPY_ORG_TURN_REPORT_TAG, d as HAPPY_ORG_SUMMARY_MAX_LENGTH, l as logger } from './api-DrijKeDb.mjs';
2
+ import { g as getPendingInteractionTimeoutMs, I as INTERACTION_SUPERSEDED_ERROR, a as INTERACTION_TIMED_OUT_ERROR } from './registerKillSessionHandler-CHEj7UjN.mjs';
3
+
4
+ async function runModeLoop(opts) {
5
+ let currentMode = opts.startingMode;
6
+ if (opts.notifyInitialMode) {
7
+ await opts.onModeChange?.(currentMode);
8
+ }
9
+ while (true) {
10
+ opts.onIteration?.(currentMode);
11
+ const result = await opts.launchers[currentMode]();
12
+ if (result.type === "exit") {
13
+ return result.value;
14
+ }
15
+ currentMode = result.mode;
16
+ await opts.onModeChange?.(currentMode);
17
+ }
18
+ }
19
+
20
+ function createKeepAliveController(opts) {
21
+ let thinking = opts.initialThinking ?? false;
22
+ let mode = opts.initialMode;
23
+ let disposed = false;
24
+ const sync = () => {
25
+ if (disposed) {
26
+ return;
27
+ }
28
+ opts.send(thinking, mode);
29
+ };
30
+ sync();
31
+ const intervalId = setInterval(sync, opts.intervalMs ?? 2e3);
32
+ return {
33
+ dispose: () => {
34
+ if (disposed) {
35
+ return;
36
+ }
37
+ disposed = true;
38
+ clearInterval(intervalId);
39
+ },
40
+ getMode: () => mode,
41
+ getThinking: () => thinking,
42
+ setMode: (nextMode) => {
43
+ mode = nextMode;
44
+ sync();
45
+ },
46
+ setThinking: (nextThinking) => {
47
+ thinking = nextThinking;
48
+ sync();
49
+ },
50
+ sync
51
+ };
52
+ }
53
+
54
+ function normalizeOptionalText(value) {
55
+ if (typeof value !== "string") {
56
+ return null;
57
+ }
58
+ const trimmed = value.trim();
59
+ return trimmed.length > 0 ? trimmed : null;
60
+ }
61
+ function normalizeSummaryText(value) {
62
+ const normalized = normalizeOptionalText(value);
63
+ return normalized ? normalized.replace(/\s+/g, " ").slice(0, HAPPY_ORG_SUMMARY_MAX_LENGTH) : null;
64
+ }
65
+ function cloneHappyOrgMetadata(happyOrg) {
66
+ return {
67
+ taskContext: happyOrg?.taskContext ? { ...happyOrg.taskContext } : void 0,
68
+ runtime: happyOrg?.runtime ? { ...happyOrg.runtime } : void 0,
69
+ activeOwner: happyOrg?.activeOwner ? { ...happyOrg.activeOwner } : null,
70
+ repeat: happyOrg?.repeat ? {
71
+ threshold: happyOrg.repeat.threshold,
72
+ fingerprints: Object.fromEntries(
73
+ Object.entries(happyOrg.repeat.fingerprints ?? {}).map(([fingerprint, entry]) => [
74
+ fingerprint,
75
+ { ...entry }
76
+ ])
77
+ )
78
+ } : void 0,
79
+ lastTurnReport: happyOrg?.lastTurnReport ? { ...happyOrg.lastTurnReport } : void 0
80
+ };
81
+ }
82
+ function normalizeHappyOrgMetadata(metadata) {
83
+ const happyOrg = cloneHappyOrgMetadata(metadata?.happyOrg);
84
+ return {
85
+ ...happyOrg,
86
+ runtime: happyOrg.runtime ?? {
87
+ status: "active",
88
+ reason: null
89
+ },
90
+ repeat: happyOrg.repeat ?? {
91
+ threshold: HAPPY_ORG_REPEAT_THRESHOLD,
92
+ fingerprints: {}
93
+ }
94
+ };
95
+ }
96
+ function withHappyOrgMetadata(metadata, happyOrg) {
97
+ return {
98
+ ...metadata,
99
+ happyOrg
100
+ };
101
+ }
102
+ function resetHappyOrgRuntimeForTask(taskContext) {
103
+ return {
104
+ taskContext,
105
+ runtime: {
106
+ status: "active",
107
+ reason: null
108
+ },
109
+ activeOwner: null,
110
+ repeat: {
111
+ threshold: HAPPY_ORG_REPEAT_THRESHOLD,
112
+ fingerprints: {}
113
+ }
114
+ };
115
+ }
116
+ function buildTerminatedStatusMessage(taskId) {
117
+ return `Task ${taskId} is terminated. Reopen it with new context, a new decision, or a new resource before continuing.`;
118
+ }
119
+ function buildMemberBusyStatusMessage(memberAgentId, activeTaskId, nextTaskId) {
120
+ return `Member ${memberAgentId} is already busy with active task ${activeTaskId}. Reject task ${nextTaskId} without continuing token usage.`;
121
+ }
122
+ function buildOwnerConflictStatusMessage(taskId, ownerAgentId) {
123
+ return `Task ${taskId} is already active under owner ${ownerAgentId}. This turn must exit without continuing token usage.`;
124
+ }
125
+ function inferDraftTurnStatus(draft) {
126
+ if (draft?.turnStatus === "turn_update" || draft?.turnStatus === "task_complete") {
127
+ return draft.turnStatus;
128
+ }
129
+ return null;
130
+ }
131
+ function inferInterventionType(draft) {
132
+ if (draft?.interventionType === "none" || draft?.interventionType === "review_needed" || draft?.interventionType === "blocker" || draft?.interventionType === "decision_needed") {
133
+ return draft.interventionType;
134
+ }
135
+ if (normalizeOptionalText(draft?.decisionNeeded)) {
136
+ return "decision_needed";
137
+ }
138
+ if (normalizeOptionalText(draft?.blockerCode)) {
139
+ return "blocker";
140
+ }
141
+ return "none";
142
+ }
143
+ function buildFallbackSummary(text, turnStatus) {
144
+ const trimmed = text.trim();
145
+ if (trimmed) {
146
+ return trimmed.replace(/\s+/g, " ").slice(0, HAPPY_ORG_SUMMARY_MAX_LENGTH);
147
+ }
148
+ return turnStatus === "turn_aborted" ? "Turn aborted before completion." : "Turn completed without a textual summary.";
149
+ }
150
+ function normalizeTurnReportDraft(draft) {
151
+ return {
152
+ turnStatus: inferDraftTurnStatus(draft),
153
+ summary: normalizeSummaryText(draft?.summary),
154
+ interventionType: inferInterventionType(draft),
155
+ blockerCode: normalizeOptionalText(draft?.blockerCode),
156
+ decisionNeeded: normalizeOptionalText(draft?.decisionNeeded),
157
+ targetArtifact: normalizeOptionalText(draft?.targetArtifact)
158
+ };
159
+ }
160
+ function stripCodeFence(text) {
161
+ return text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
162
+ }
163
+ function extractTaggedTurnReport(text) {
164
+ const matcher = new RegExp(
165
+ `<${HAPPY_ORG_TURN_REPORT_TAG}>\\s*([\\s\\S]*?)\\s*</${HAPPY_ORG_TURN_REPORT_TAG}>`,
166
+ "gi"
167
+ );
168
+ let lastMatch = null;
169
+ for (let match = matcher.exec(text); match; match = matcher.exec(text)) {
170
+ lastMatch = match;
171
+ }
172
+ if (!lastMatch) {
173
+ return {
174
+ cleanedText: text.trim(),
175
+ draft: null
176
+ };
177
+ }
178
+ const rawBlock = stripCodeFence(lastMatch[1] ?? "");
179
+ let draft = null;
180
+ try {
181
+ const parsed = JSON.parse(rawBlock);
182
+ draft = {
183
+ turnStatus: normalizeOptionalText(parsed.turnStatus),
184
+ summary: normalizeSummaryText(parsed.summary),
185
+ interventionType: normalizeOptionalText(parsed.interventionType),
186
+ blockerCode: normalizeOptionalText(parsed.blockerCode),
187
+ decisionNeeded: normalizeOptionalText(parsed.decisionNeeded),
188
+ targetArtifact: normalizeOptionalText(parsed.targetArtifact)
189
+ };
190
+ } catch {
191
+ draft = null;
192
+ }
193
+ const cleanedText = `${text.slice(0, lastMatch.index)}${text.slice(lastMatch.index + lastMatch[0].length)}`.replace(/\n{3,}/g, "\n\n").trim();
194
+ return {
195
+ cleanedText,
196
+ draft
197
+ };
198
+ }
199
+ function buildRepeatFingerprint(context, blockerCode, targetArtifact) {
200
+ if (!blockerCode) {
201
+ return null;
202
+ }
203
+ return [
204
+ context.taskId,
205
+ context.memberAgentId,
206
+ blockerCode,
207
+ targetArtifact ?? ""
208
+ ].join("::");
209
+ }
210
+ function resolveReportedTurnStatus(transportTurnStatus, draft) {
211
+ if (transportTurnStatus === "turn_aborted") {
212
+ return "turn_aborted";
213
+ }
214
+ return draft.turnStatus === "task_complete" ? "task_complete" : "turn_update";
215
+ }
216
+ function buildRuntimeStateAfterTurn(report) {
217
+ if (report.turnStatus === "task_complete") {
218
+ return {
219
+ status: "waiting_close",
220
+ reason: "awaiting_ceo_close"
221
+ };
222
+ }
223
+ switch (report.interventionType) {
224
+ case "decision_needed":
225
+ return {
226
+ status: "waiting_decision",
227
+ reason: "awaiting_user_decision"
228
+ };
229
+ case "review_needed":
230
+ return {
231
+ status: "waiting_review",
232
+ reason: "awaiting_ceo_review"
233
+ };
234
+ case "blocker":
235
+ return {
236
+ status: "waiting_review",
237
+ reason: "awaiting_ceo_context"
238
+ };
239
+ default:
240
+ return {
241
+ status: "active",
242
+ reason: null
243
+ };
244
+ }
245
+ }
246
+ function buildHappyOrgTurnPrompt(prompt, turn) {
247
+ const reopenLines = turn.reopenContext ? [
248
+ "",
249
+ "This task was explicitly reopened for this turn with the following new inputs:",
250
+ turn.reopenContext.newContext ? `- newContext: ${turn.reopenContext.newContext}` : null,
251
+ turn.reopenContext.newDecision ? `- newDecision: ${turn.reopenContext.newDecision}` : null,
252
+ turn.reopenContext.newResource ? `- newResource: ${turn.reopenContext.newResource}` : null
253
+ ].filter(Boolean) : [];
254
+ const header = [
255
+ "[HAPPY_ORG_TASK_CONTEXT]",
256
+ `taskId=${turn.context.taskId}`,
257
+ `organizationId=${turn.context.organizationId}`,
258
+ `memberAgentId=${turn.context.memberAgentId}`,
259
+ `supervisorAgentId=${turn.context.supervisorAgentId}`,
260
+ "Stay on this exact task for the whole turn.",
261
+ "End your response with exactly one raw JSON block inside these tags and do not wrap it in a markdown code fence:",
262
+ `<${HAPPY_ORG_TURN_REPORT_TAG}>{"turnStatus":"turn_update","summary":"short task-board summary","interventionType":"none","blockerCode":null,"decisionNeeded":null,"targetArtifact":null}</${HAPPY_ORG_TURN_REPORT_TAG}>`,
263
+ "Allowed turnStatus values in the JSON block: turn_update, task_complete.",
264
+ "Allowed interventionType values: none, review_needed, blocker, decision_needed.",
265
+ "Use turnStatus=task_complete only when you believe the assigned task is finished and ready for CEO acceptance.",
266
+ "If turnStatus=task_complete, the task will wait for CEO close; do not treat it as automatically closed.",
267
+ "If turnStatus=task_complete, interventionType must be review_needed.",
268
+ `summary must fit on a one-line CEO card or task-board card: short, actionable, no long process logs, max ${HAPPY_ORG_SUMMARY_MAX_LENGTH} characters.`,
269
+ "Use blocker only for problems the organization may still solve internally.",
270
+ "Use decision_needed only when a CEO or user decision is required.",
271
+ "Use review_needed when supervisor review is needed but the work is not blocked.",
272
+ ...reopenLines,
273
+ "[/HAPPY_ORG_TASK_CONTEXT]",
274
+ "",
275
+ prompt
276
+ ];
277
+ return header.join("\n");
278
+ }
279
+ function resolveHappyOrgQueuedTurn(opts) {
280
+ const metadata = opts.metadata ?? null;
281
+ if (!metadata) {
282
+ return {
283
+ nextMetadata: null,
284
+ queuedTurn: null,
285
+ blocked: false
286
+ };
287
+ }
288
+ const currentHappyOrg = normalizeHappyOrgMetadata(metadata);
289
+ let nextHappyOrg = cloneHappyOrgMetadata(currentHappyOrg);
290
+ const messageHappyOrg = opts.message.meta?.happyOrg;
291
+ const now = opts.now?.() ?? Date.now();
292
+ const createRunId = opts.createRunId ?? ((taskContext2, currentNow) => `${taskContext2.taskId}:${taskContext2.memberAgentId}:${currentNow}`);
293
+ if (messageHappyOrg?.taskContext) {
294
+ const currentTaskId = currentHappyOrg.taskContext?.taskId ?? null;
295
+ if (currentTaskId !== messageHappyOrg.taskContext.taskId) {
296
+ if (currentHappyOrg.taskContext && currentHappyOrg.runtime?.status === "active") {
297
+ return {
298
+ nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
299
+ queuedTurn: null,
300
+ blocked: true,
301
+ statusMessage: buildMemberBusyStatusMessage(
302
+ currentHappyOrg.taskContext.memberAgentId,
303
+ currentHappyOrg.taskContext.taskId,
304
+ messageHappyOrg.taskContext.taskId
305
+ )
306
+ };
307
+ }
308
+ nextHappyOrg = resetHappyOrgRuntimeForTask(messageHappyOrg.taskContext);
309
+ } else {
310
+ nextHappyOrg.taskContext = { ...messageHappyOrg.taskContext };
311
+ }
312
+ }
313
+ const taskContext = nextHappyOrg.taskContext ?? null;
314
+ if (!taskContext) {
315
+ return {
316
+ nextMetadata: metadata,
317
+ queuedTurn: null,
318
+ blocked: false
319
+ };
320
+ }
321
+ const control = messageHappyOrg?.control;
322
+ if (control?.action === "terminate") {
323
+ nextHappyOrg.runtime = {
324
+ status: "terminated",
325
+ reason: normalizeOptionalText(control.reason) ?? "terminated_by_supervisor",
326
+ terminatedAt: now
327
+ };
328
+ nextHappyOrg.activeOwner = null;
329
+ return {
330
+ nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
331
+ queuedTurn: null,
332
+ blocked: true,
333
+ statusMessage: buildTerminatedStatusMessage(taskContext.taskId)
334
+ };
335
+ }
336
+ const hasReopenInputs = Boolean(
337
+ normalizeOptionalText(control?.newContext) || normalizeOptionalText(control?.newDecision) || normalizeOptionalText(control?.newResource)
338
+ );
339
+ if (nextHappyOrg.runtime?.status === "terminated") {
340
+ if (control?.action !== "reopen") {
341
+ return {
342
+ nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
343
+ queuedTurn: null,
344
+ blocked: true,
345
+ statusMessage: buildTerminatedStatusMessage(taskContext.taskId)
346
+ };
347
+ }
348
+ if (!hasReopenInputs) {
349
+ return {
350
+ nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
351
+ queuedTurn: null,
352
+ blocked: true,
353
+ statusMessage: `Task ${taskContext.taskId} can only reopen with new context, a new decision, or a new resource.`
354
+ };
355
+ }
356
+ }
357
+ const reopenContext = control?.action === "reopen" && hasReopenInputs ? {
358
+ newContext: normalizeOptionalText(control.newContext),
359
+ newDecision: normalizeOptionalText(control.newDecision),
360
+ newResource: normalizeOptionalText(control.newResource)
361
+ } : void 0;
362
+ if (reopenContext) {
363
+ nextHappyOrg.runtime = {
364
+ status: "active",
365
+ reason: null,
366
+ reopenedAt: now
367
+ };
368
+ } else if (!nextHappyOrg.runtime || nextHappyOrg.runtime.status !== "active") {
369
+ nextHappyOrg.runtime = {
370
+ status: "active",
371
+ reason: null
372
+ };
373
+ }
374
+ if (!nextHappyOrg.activeOwner) {
375
+ nextHappyOrg.activeOwner = {
376
+ ownerAgentId: taskContext.memberAgentId,
377
+ ownerRunId: createRunId(taskContext, now),
378
+ claimedAt: now
379
+ };
380
+ } else if (nextHappyOrg.activeOwner.ownerAgentId !== taskContext.memberAgentId) {
381
+ return {
382
+ nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
383
+ queuedTurn: null,
384
+ blocked: true,
385
+ statusMessage: buildOwnerConflictStatusMessage(
386
+ taskContext.taskId,
387
+ nextHappyOrg.activeOwner.ownerAgentId
388
+ )
389
+ };
390
+ }
391
+ return {
392
+ nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
393
+ queuedTurn: {
394
+ context: taskContext,
395
+ ...reopenContext ? { reopenContext } : {}
396
+ },
397
+ blocked: false
398
+ };
399
+ }
400
+ function finalizeHappyOrgTurn(opts) {
401
+ const metadata = opts.metadata ?? null;
402
+ const queuedTurn = opts.queuedTurn ?? null;
403
+ const { cleanedText, draft } = extractTaggedTurnReport(opts.responseText);
404
+ if (!metadata || !queuedTurn) {
405
+ return {
406
+ cleanedText,
407
+ report: null,
408
+ nextMetadata: metadata
409
+ };
410
+ }
411
+ const now = opts.now?.() ?? Date.now();
412
+ const normalizedDraft = normalizeTurnReportDraft(draft);
413
+ const reportTurnStatus = resolveReportedTurnStatus(opts.turnStatus, normalizedDraft);
414
+ const report = {
415
+ ...queuedTurn.context,
416
+ turnStatus: reportTurnStatus,
417
+ interventionType: reportTurnStatus === "task_complete" ? "review_needed" : normalizedDraft.interventionType ?? "none",
418
+ summary: normalizedDraft.summary ?? buildFallbackSummary(cleanedText, reportTurnStatus),
419
+ blockerCode: normalizedDraft.blockerCode ?? null,
420
+ decisionNeeded: normalizedDraft.decisionNeeded ?? null,
421
+ targetArtifact: normalizedDraft.targetArtifact ?? null,
422
+ repeatFingerprint: buildRepeatFingerprint(
423
+ queuedTurn.context,
424
+ normalizedDraft.blockerCode ?? null,
425
+ normalizedDraft.targetArtifact ?? null
426
+ )
427
+ };
428
+ const nextHappyOrg = normalizeHappyOrgMetadata(metadata);
429
+ nextHappyOrg.taskContext = { ...queuedTurn.context };
430
+ nextHappyOrg.lastTurnReport = report;
431
+ nextHappyOrg.activeOwner = null;
432
+ nextHappyOrg.repeat = nextHappyOrg.repeat ?? {
433
+ threshold: HAPPY_ORG_REPEAT_THRESHOLD,
434
+ fingerprints: {}
435
+ };
436
+ let terminateMessage;
437
+ if (report.repeatFingerprint) {
438
+ const currentEntry = nextHappyOrg.repeat.fingerprints[report.repeatFingerprint] ?? {
439
+ count: 0};
440
+ const nextCount = currentEntry.count + 1;
441
+ nextHappyOrg.repeat.fingerprints[report.repeatFingerprint] = {
442
+ count: nextCount,
443
+ lastSeenAt: now
444
+ };
445
+ if (nextCount >= nextHappyOrg.repeat.threshold) {
446
+ nextHappyOrg.runtime = {
447
+ status: "terminated",
448
+ reason: `repeat_fingerprint:${report.repeatFingerprint}`,
449
+ terminatedAt: now
450
+ };
451
+ terminateMessage = `Task ${queuedTurn.context.taskId} hit repeat threshold for ${report.repeatFingerprint} and is now terminated until reopen.`;
452
+ } else if (!nextHappyOrg.runtime || nextHappyOrg.runtime.status !== "terminated") {
453
+ nextHappyOrg.runtime = buildRuntimeStateAfterTurn(report);
454
+ }
455
+ } else if (!nextHappyOrg.runtime || nextHappyOrg.runtime.status !== "terminated") {
456
+ nextHappyOrg.runtime = buildRuntimeStateAfterTurn(report);
457
+ }
458
+ return {
459
+ cleanedText,
460
+ report,
461
+ nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
462
+ terminateMessage
463
+ };
464
+ }
465
+
466
+ class ProviderSelectionHandler {
467
+ pendingRequests = /* @__PURE__ */ new Map();
468
+ session;
469
+ providerLabel;
470
+ constructor(session, providerLabel) {
471
+ this.session = session;
472
+ this.providerLabel = providerLabel;
473
+ this.setupRpcHandler();
474
+ }
475
+ updateSession(newSession) {
476
+ this.session = newSession;
477
+ this.setupRpcHandler();
478
+ }
479
+ async requestSelection(request) {
480
+ return new Promise((resolve, reject) => {
481
+ const pending = {
482
+ resolve,
483
+ reject,
484
+ request
485
+ };
486
+ pending.timeoutHandle = setTimeout(() => {
487
+ this.handleSelectionTimeout(request.id, pending);
488
+ }, getPendingInteractionTimeoutMs());
489
+ this.pendingRequests.set(request.id, pending);
490
+ this.session.updateAgentState((currentState) => ({
491
+ ...currentState,
492
+ requests: {
493
+ ...currentState.requests,
494
+ [request.id]: {
495
+ tool: "AskUserQuestion",
496
+ arguments: {
497
+ requestKind: "selection",
498
+ questions: [
499
+ {
500
+ header: this.providerLabel,
501
+ question: request.message,
502
+ multiSelect: false,
503
+ options: request.options.map((option) => ({
504
+ label: option.label,
505
+ description: option.description || option.optionId,
506
+ optionId: option.optionId
507
+ }))
508
+ }
509
+ ]
510
+ },
511
+ createdAt: Date.now(),
512
+ requestKind: "selection",
513
+ options: request.options,
514
+ defaultOptionId: request.defaultOptionId
515
+ }
516
+ }
517
+ }));
518
+ logger.debug(`[${this.providerLabel}] Selection request sent (${request.id}) with ${request.options.length} options`);
519
+ });
520
+ }
521
+ hasPendingRequests() {
522
+ return this.pendingRequests.size > 0;
523
+ }
524
+ supersedePendingRequests(reason = INTERACTION_SUPERSEDED_ERROR) {
525
+ const pendingSnapshot = Array.from(this.pendingRequests.entries());
526
+ if (pendingSnapshot.length === 0) {
527
+ return 0;
528
+ }
529
+ this.pendingRequests.clear();
530
+ const completedAt = Date.now();
531
+ for (const [, pending] of pendingSnapshot) {
532
+ this.clearPendingRequestTimeout(pending);
533
+ pending.reject(new Error(reason));
534
+ }
535
+ this.session.updateAgentState((currentState) => {
536
+ const requests = { ...currentState.requests || {} };
537
+ const completedRequests = { ...currentState.completedRequests || {} };
538
+ for (const [id, request] of Object.entries(requests)) {
539
+ if (request.requestKind !== "selection") {
540
+ continue;
541
+ }
542
+ completedRequests[id] = {
543
+ ...request,
544
+ completedAt,
545
+ status: "canceled",
546
+ reason,
547
+ requestKind: "selection"
548
+ };
549
+ delete requests[id];
550
+ }
551
+ return {
552
+ ...currentState,
553
+ requests,
554
+ completedRequests
555
+ };
556
+ });
557
+ logger.debug(`[${this.providerLabel}] Superseded ${pendingSnapshot.length} pending selection request(s)`);
558
+ return pendingSnapshot.length;
559
+ }
560
+ reset(reason = "Session reset") {
561
+ const pendingSnapshot = Array.from(this.pendingRequests.entries());
562
+ this.pendingRequests.clear();
563
+ for (const [, pending] of pendingSnapshot) {
564
+ this.clearPendingRequestTimeout(pending);
565
+ pending.reject(new Error(reason));
566
+ }
567
+ this.session.updateAgentState((currentState) => {
568
+ const requests = { ...currentState.requests || {} };
569
+ const completedRequests = { ...currentState.completedRequests || {} };
570
+ for (const [id, request] of Object.entries(requests)) {
571
+ if (request.requestKind !== "selection") {
572
+ continue;
573
+ }
574
+ completedRequests[id] = {
575
+ ...request,
576
+ completedAt: Date.now(),
577
+ status: "canceled",
578
+ reason,
579
+ requestKind: "selection"
580
+ };
581
+ delete requests[id];
582
+ }
583
+ return {
584
+ ...currentState,
585
+ requests,
586
+ completedRequests
587
+ };
588
+ });
589
+ }
590
+ setupRpcHandler() {
591
+ this.session.rpcHandlerManager.registerHandler("selection", async (response) => {
592
+ const pending = this.pendingRequests.get(response.id);
593
+ if (!pending) {
594
+ logger.debug(`[${this.providerLabel}] Selection request not found or already resolved`);
595
+ return;
596
+ }
597
+ this.pendingRequests.delete(response.id);
598
+ this.clearPendingRequestTimeout(pending);
599
+ pending.resolve(response);
600
+ this.session.updateAgentState((currentState) => {
601
+ const request = currentState.requests?.[response.id];
602
+ if (!request) {
603
+ return currentState;
604
+ }
605
+ const { [response.id]: _, ...remainingRequests } = currentState.requests || {};
606
+ return {
607
+ ...currentState,
608
+ requests: remainingRequests,
609
+ completedRequests: {
610
+ ...currentState.completedRequests,
611
+ [response.id]: {
612
+ ...request,
613
+ completedAt: Date.now(),
614
+ status: "approved",
615
+ requestKind: "selection",
616
+ selectedOptionId: response.optionId
617
+ }
618
+ }
619
+ };
620
+ });
621
+ });
622
+ }
623
+ clearPendingRequestTimeout(pending) {
624
+ if (pending?.timeoutHandle) {
625
+ clearTimeout(pending.timeoutHandle);
626
+ pending.timeoutHandle = void 0;
627
+ }
628
+ }
629
+ handleSelectionTimeout(requestId, pending) {
630
+ const active = this.pendingRequests.get(requestId);
631
+ if (!active || active !== pending) {
632
+ return;
633
+ }
634
+ this.pendingRequests.delete(requestId);
635
+ this.clearPendingRequestTimeout(active);
636
+ active.reject(new Error(INTERACTION_TIMED_OUT_ERROR));
637
+ this.session.updateAgentState((currentState) => {
638
+ const request = currentState.requests?.[requestId] || {
639
+ tool: "AskUserQuestion",
640
+ arguments: {
641
+ requestKind: "selection",
642
+ questions: []
643
+ },
644
+ createdAt: Date.now(),
645
+ requestKind: "selection",
646
+ options: active.request.options,
647
+ defaultOptionId: active.request.defaultOptionId
648
+ };
649
+ const { [requestId]: _, ...remainingRequests } = currentState.requests || {};
650
+ return {
651
+ ...currentState,
652
+ requests: remainingRequests,
653
+ completedRequests: {
654
+ ...currentState.completedRequests,
655
+ [requestId]: {
656
+ ...request,
657
+ completedAt: Date.now(),
658
+ status: "canceled",
659
+ reason: INTERACTION_TIMED_OUT_ERROR,
660
+ requestKind: "selection"
661
+ }
662
+ }
663
+ };
664
+ });
665
+ this.session.sendSessionEvent({
666
+ type: "message",
667
+ message: "Pending interaction timed out waiting for a response. Send a new message to continue."
668
+ });
669
+ logger.debug(`[${this.providerLabel}] Selection request timed out (${requestId})`);
670
+ }
671
+ }
672
+
673
+ export { ProviderSelectionHandler as P, resolveHappyOrgQueuedTurn as a, buildHappyOrgTurnPrompt as b, createKeepAliveController as c, finalizeHappyOrgTurn as f, runModeLoop as r };