happy-imou-cloud 2.1.49 → 2.1.51

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 (55) hide show
  1. package/dist/AcpBackend-CqO3D07V.mjs +2619 -0
  2. package/dist/AcpBackend-XPiTd6ph.cjs +2621 -0
  3. package/dist/{BaseReasoningProcessor-Dn9NcoHz.cjs → BaseReasoningProcessor-BD9tiwep.cjs} +1 -144
  4. package/dist/{BaseReasoningProcessor-CAVeOdyo.mjs → BaseReasoningProcessor-CjlayL2f.mjs} +2 -144
  5. package/dist/ConversationHistory-Bl2doTA-.cjs +780 -0
  6. package/dist/ConversationHistory-CI5bBfuA.mjs +771 -0
  7. package/dist/{ProviderSelectionHandler-BJJc7qOR.cjs → ProviderSelectionHandler-C7GE5QjX.cjs} +6 -6
  8. package/dist/{ProviderSelectionHandler-DIYidT13.mjs → ProviderSelectionHandler-uQ8jzdzr.mjs} +2 -2
  9. package/dist/RuntimeShell-BDt42io_.mjs +252 -0
  10. package/dist/RuntimeShell-D_Te12wq.cjs +258 -0
  11. package/dist/bootstrapManagedProviderSession-Bln-TwyB.cjs +147 -0
  12. package/dist/bootstrapManagedProviderSession-D2Z6YU3n.mjs +145 -0
  13. package/dist/claude-BKNT-2fG.cjs +1080 -0
  14. package/dist/claude-CnN5WCWj.mjs +1073 -0
  15. package/dist/codex-DLGP8WF6.mjs +577 -0
  16. package/dist/codex-Fv2eali8.cjs +582 -0
  17. package/dist/{command-VcH4hbhi.cjs → command-BWPlJyCN.cjs} +16 -8
  18. package/dist/{command-CzfRRhVe.mjs → command-CELwsYoG.mjs} +15 -7
  19. package/dist/config-CFL0Gkqt.cjs +184 -0
  20. package/dist/config-ChSPe7p9.mjs +174 -0
  21. package/dist/createDefaultRuntimeShell-BXu3vCvT.cjs +33 -0
  22. package/dist/createDefaultRuntimeShell-DOg6g3-G.mjs +31 -0
  23. package/dist/cursor-Blq1cHdr.cjs +91 -0
  24. package/dist/cursor-CwPNSy_A.mjs +88 -0
  25. package/dist/future-Dq4Ha1Dn.cjs +24 -0
  26. package/dist/future-xRdLl3vf.mjs +22 -0
  27. package/dist/{index-xa1kwZoj.cjs → index-B_JYgMUS.cjs} +189 -5352
  28. package/dist/{index-7Z93BoVn.mjs → index-CX-F_fuk.mjs} +177 -5331
  29. package/dist/index.cjs +2 -2
  30. package/dist/index.mjs +2 -2
  31. package/dist/installFatalProcessHandlers-0vaw9MAz.mjs +55 -0
  32. package/dist/installFatalProcessHandlers-CyURn5Bp.cjs +57 -0
  33. package/dist/launch-BoCCEd5p.mjs +63 -0
  34. package/dist/launch-wZA5BcvS.cjs +66 -0
  35. package/dist/lib.cjs +2 -3
  36. package/dist/lib.d.cts +20 -17
  37. package/dist/lib.d.mts +20 -17
  38. package/dist/lib.mjs +1 -2
  39. package/dist/resolveCommand-B3BGyBE2.mjs +189 -0
  40. package/dist/resolveCommand-DYMd9PNC.cjs +193 -0
  41. package/dist/{runClaude-zCwRhpOw.mjs → runClaude-Be0myF9k.mjs} +8 -5
  42. package/dist/{runClaude-BBGNmGj6.cjs → runClaude-DZJt5er7.cjs} +46 -43
  43. package/dist/{runCodex-BbgLVjb9.mjs → runCodex-BSnyN4m7.mjs} +226 -117
  44. package/dist/{runCodex-jUU6U2tZ.cjs → runCodex-DTCcGRue.cjs} +269 -160
  45. package/dist/runCursor-Bn1PuwJy.cjs +506 -0
  46. package/dist/runCursor-M6dQ6bGF.mjs +504 -0
  47. package/dist/{runGemini-DcwNsudA.mjs → runGemini-BNm4vYKA.mjs} +279 -5
  48. package/dist/{runGemini-C0NT8MHK.cjs → runGemini-Bn3lFhz6.cjs} +309 -35
  49. package/dist/{registerKillSessionHandler-DLDg2EES.mjs → sessionControl-1bT_7OI6.mjs} +1643 -2405
  50. package/dist/{registerKillSessionHandler-CfCya6si.cjs → sessionControl-flKnQrx0.cjs} +1647 -2417
  51. package/dist/{api-DnqaNvyV.mjs → types-B5vtxa38.mjs} +55 -5
  52. package/dist/{api-D7nAeZi7.cjs → types-CttABk32.cjs} +55 -4
  53. package/package.json +2 -2
  54. package/dist/types-CiliQpqS.mjs +0 -52
  55. package/dist/types-DVk3crez.cjs +0 -54
@@ -1,10 +1,11 @@
1
- import { m as initialMachineMetadata, R as RuntimeShell, f as formatDisplayMessage, n as resolveCanonicalToolNameV2, o as isTerminalReferenceOnlyPayload, t as truncateDisplayMessage } from './index-7Z93BoVn.mjs';
2
- import { r as readSettings, H as HeadTailPreviewBuffer, e as HAPPY_ORG_REPLY_ACK_VERSION, f as HAPPY_ORG_TURN_REPORT_TAG, g as HAPPY_ORG_SUMMARY_MAX_LENGTH, j as HAPPY_ORG_REPEAT_THRESHOLD, l as logger, n as normalizePreviewableArtifactTarget } from './api-DnqaNvyV.mjs';
1
+ import { o as initialMachineMetadata, q as resolveCanonicalToolNameV2 } from './index-CX-F_fuk.mjs';
2
+ import { r as readSettings, H as HeadTailPreviewBuffer, l as logger, f as HAPPY_ORG_REPLY_ACK_VERSION, g as HAPPY_ORG_TURN_REPORT_TAG, j as HAPPY_ORG_SUMMARY_MAX_LENGTH, k as HAPPY_ORG_REPEAT_THRESHOLD, n as normalizePreviewableArtifactTarget } from './types-B5vtxa38.mjs';
3
3
  import { randomUUID } from 'node:crypto';
4
- import { basename } from 'node:path';
4
+ import { f as formatDisplayMessage, i as isTerminalReferenceOnlyPayload } from './RuntimeShell-BDt42io_.mjs';
5
5
  import 'axios';
6
6
  import 'node:events';
7
7
  import 'node:fs/promises';
8
+ import { basename } from 'node:path';
8
9
  import 'socket.io-client';
9
10
  import 'tweetnacl';
10
11
  import 'fs/promises';
@@ -13,7 +14,6 @@ import 'path';
13
14
  import 'node:child_process';
14
15
  import 'chalk';
15
16
  import 'expo-server-sdk';
16
- import './types-CiliQpqS.mjs';
17
17
 
18
18
  class MissingMachineIdError extends Error {
19
19
  constructor(message) {
@@ -34,27 +34,6 @@ async function ensureManagedProviderMachine(opts) {
34
34
  return machineId;
35
35
  }
36
36
 
37
- async function launchRuntimeHandleWithFactoryResult(opts) {
38
- const shell = opts.shell ?? new RuntimeShell();
39
- let factoryResult;
40
- const session = await shell.launch({
41
- provider: opts.provider,
42
- cwd: opts.cwd,
43
- env: opts.env,
44
- createBackend: (factoryOpts) => {
45
- factoryResult = opts.createBackendResult(factoryOpts);
46
- return factoryResult.backend;
47
- }
48
- });
49
- if (factoryResult === void 0) {
50
- throw new Error(`Runtime provider "${opts.provider}" did not create a backend result`);
51
- }
52
- return {
53
- session,
54
- factoryResult
55
- };
56
- }
57
-
58
37
  function inferToolResultError(result) {
59
38
  if (!result || typeof result !== "object") {
60
39
  return false;
@@ -119,11 +98,11 @@ function prepareTerminalOutputForForwarding(value) {
119
98
  }
120
99
 
121
100
  const DISPLAY_FRIENDLY_TOOL_FIELDS = ["stdout", "stderr", "output", "text", "message", "detail", "reason", "data"];
122
- function isRecord$2(value) {
101
+ function isRecord(value) {
123
102
  return !!value && typeof value === "object" && !Array.isArray(value);
124
103
  }
125
104
  function stripInternalToolMeta(value) {
126
- if (!isRecord$2(value)) {
105
+ if (!isRecord(value)) {
127
106
  return value;
128
107
  }
129
108
  const {
@@ -144,7 +123,7 @@ function extractNestedTextContent(value) {
144
123
  const parts = value.map((item) => extractNestedTextContent(item)).filter((item) => typeof item === "string" && item.length > 0);
145
124
  return parts.length > 0 ? parts.join("\n") : null;
146
125
  }
147
- if (!isRecord$2(value)) {
126
+ if (!isRecord(value)) {
148
127
  return null;
149
128
  }
150
129
  if (typeof value.text === "string" && value.text.trim().length > 0) {
@@ -164,7 +143,7 @@ function humanizeToolLabel(rawToolName) {
164
143
  return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
165
144
  }
166
145
  function isToolLifecycleResult(value) {
167
- if (!isRecord$2(value)) {
146
+ if (!isRecord(value)) {
168
147
  return false;
169
148
  }
170
149
  return typeof value.state === "string" && ("messages" in value || "description" in value || "createdAt" in value || "startedAt" in value || "completedAt" in value);
@@ -213,7 +192,7 @@ function normalizeCodexToolOutput(rawToolName, value) {
213
192
  if (isTerminalReferenceOnlyPayload(normalizedLifecyclePayload)) {
214
193
  return void 0;
215
194
  }
216
- if (!isRecord$2(normalizedLifecyclePayload)) {
195
+ if (!isRecord(normalizedLifecyclePayload)) {
217
196
  return normalizedLifecyclePayload;
218
197
  }
219
198
  const hasDisplayFriendlyField = DISPLAY_FRIENDLY_TOOL_FIELDS.some((field) => field in normalizedLifecyclePayload);
@@ -240,7 +219,7 @@ function truncateProviderOutputValue(value, label, seen = /* @__PURE__ */ new We
240
219
  if (Array.isArray(value)) {
241
220
  return renderOutputPreview(value, label);
242
221
  }
243
- if (!isRecord$2(value)) {
222
+ if (!isRecord(value)) {
244
223
  return value;
245
224
  }
246
225
  if (seen.has(value)) {
@@ -264,6 +243,8 @@ function getDefaultExecToolName(provider) {
264
243
  return "ClaudeBash";
265
244
  case "codex":
266
245
  return "CodexBash";
246
+ case "cursor":
247
+ return "CursorBash";
267
248
  case "gemini":
268
249
  return "GeminiBash";
269
250
  }
@@ -274,6 +255,8 @@ function getDefaultPatchToolName(provider) {
274
255
  return "ClaudePatch";
275
256
  case "codex":
276
257
  return "CodexPatch";
258
+ case "cursor":
259
+ return "CursorPatch";
277
260
  case "gemini":
278
261
  return "GeminiPatch";
279
262
  }
@@ -311,7 +294,7 @@ function hasDisplayPayload(value) {
311
294
  if (Array.isArray(sanitized)) {
312
295
  return sanitized.length > 0;
313
296
  }
314
- if (isRecord$2(sanitized)) {
297
+ if (isRecord(sanitized)) {
315
298
  return Object.keys(sanitized).length > 0;
316
299
  }
317
300
  return true;
@@ -470,2558 +453,1813 @@ async function waitForResponseCompleteWithAbort(backend, signal, timeoutMs) {
470
453
  });
471
454
  }
472
455
 
473
- function supportsAgentStateUpdateEvents(sessionClient) {
474
- return typeof sessionClient.once === "function" && typeof sessionClient.off === "function";
475
- }
476
- async function syncControlledByUserState(sessionClient, controlledByUser) {
477
- if (!supportsAgentStateUpdateEvents(sessionClient)) {
478
- sessionClient.updateAgentState((currentState) => ({
479
- ...currentState,
480
- controlledByUser
481
- }));
482
- return;
483
- }
484
- await new Promise((resolve) => {
485
- let settled = false;
486
- const handleUpdated = () => {
487
- if (settled) {
488
- return;
489
- }
490
- settled = true;
491
- clearTimeout(timeout);
492
- sessionClient.off("agent-state-updated", handleUpdated);
493
- resolve();
494
- };
495
- const timeout = setTimeout(() => {
496
- if (settled) {
497
- return;
498
- }
499
- settled = true;
500
- sessionClient.off("agent-state-updated", handleUpdated);
501
- resolve();
502
- }, 1500);
503
- sessionClient.once("agent-state-updated", handleUpdated);
504
- sessionClient.updateAgentState((currentState) => ({
505
- ...currentState,
506
- controlledByUser
507
- }));
508
- });
509
- }
510
-
511
- const SESSION_LABEL_MAX_LENGTH = 60;
512
- const TITLE_MAX_LENGTH = 80;
513
- const BODY_MAX_LENGTH = 140;
514
- function isRecord$1(value) {
515
- return !!value && typeof value === "object" && !Array.isArray(value);
516
- }
517
- function normalizeText(value) {
518
- if (typeof value !== "string") {
519
- return null;
520
- }
521
- const normalized = value.replace(/\s+/g, " ").trim();
522
- return normalized.length > 0 ? normalized : null;
523
- }
524
- function truncateText(value, maxLength) {
525
- if (value.length <= maxLength) {
526
- return value;
527
- }
528
- return `${value.slice(0, maxLength - 1).trimEnd()}\u2026`;
529
- }
530
- function toProviderKey(providerLabel) {
531
- const normalized = normalizeText(providerLabel)?.toLowerCase();
532
- if (!normalized) {
533
- return "unknown";
534
- }
535
- if (normalized === "claude" || normalized === "claude code" || normalized === "cloud code") {
536
- return "claude";
537
- }
538
- if (normalized === "codex") {
539
- return "codex";
540
- }
541
- if (normalized === "gemini") {
542
- return "gemini";
456
+ const INTERACTION_SUPERSEDED_ERROR = "Interaction superseded by new user message";
457
+ const INTERACTION_TIMED_OUT_ERROR = "Interaction timed out waiting for user response";
458
+ const DEFAULT_INTERACTION_TIMEOUT_MS = 2 * 60 * 1e3;
459
+ function getPendingInteractionTimeoutMs() {
460
+ const raw = Number(process.env.HAPPY_INTERACTION_TIMEOUT_MS);
461
+ if (Number.isFinite(raw) && raw > 0) {
462
+ return raw;
543
463
  }
544
- return normalized.replace(/\s+/g, "-");
464
+ return DEFAULT_INTERACTION_TIMEOUT_MS;
545
465
  }
546
- function deriveNotificationProviderLabel(providerLabel) {
547
- const normalized = normalizeText(providerLabel)?.toLowerCase();
548
- if (!normalized) {
549
- return "Agent";
466
+ class BasePermissionHandler {
467
+ pendingRequests = /* @__PURE__ */ new Map();
468
+ session;
469
+ isResetting = false;
470
+ constructor(session) {
471
+ this.session = session;
472
+ this.setupRpcHandler();
550
473
  }
551
- if (normalized === "claude" || normalized === "claude code" || normalized === "cloud code") {
552
- return "Claude";
474
+ /**
475
+ * Update the session reference (used after offline reconnection swaps sessions).
476
+ * This is critical for avoiding stale session references after onSessionSwap.
477
+ */
478
+ updateSession(newSession) {
479
+ logger.debug(`${this.getLogPrefix()} Session reference updated`);
480
+ this.session = newSession;
481
+ this.setupRpcHandler();
553
482
  }
554
- if (normalized === "codex") {
555
- return "Codex";
483
+ /**
484
+ * Setup RPC handler for permission responses.
485
+ */
486
+ setupRpcHandler() {
487
+ this.session.rpcHandlerManager.registerHandler(
488
+ "permission",
489
+ async (response) => {
490
+ const pending = this.pendingRequests.get(response.id);
491
+ if (!pending) {
492
+ logger.debug(`${this.getLogPrefix()} Permission request not found or already resolved`);
493
+ return;
494
+ }
495
+ this.pendingRequests.delete(response.id);
496
+ this.clearPendingRequestTimeout(pending);
497
+ const result = response.approved ? { decision: response.decision === "approved_for_session" ? "approved_for_session" : "approved" } : { decision: response.decision === "denied" ? "denied" : "abort" };
498
+ pending.resolve(result);
499
+ this.session.updateAgentState((currentState) => {
500
+ const request = currentState.requests?.[response.id];
501
+ if (!request) return currentState;
502
+ const { [response.id]: _, ...remainingRequests } = currentState.requests || {};
503
+ let res = {
504
+ ...currentState,
505
+ requests: remainingRequests,
506
+ completedRequests: {
507
+ ...currentState.completedRequests,
508
+ [response.id]: {
509
+ ...request,
510
+ completedAt: Date.now(),
511
+ status: response.approved ? "approved" : "denied",
512
+ decision: result.decision
513
+ }
514
+ }
515
+ };
516
+ return res;
517
+ });
518
+ logger.debug(`${this.getLogPrefix()} Permission ${response.approved ? "approved" : "denied"} for ${pending.toolName}`);
519
+ }
520
+ );
556
521
  }
557
- if (normalized === "gemini") {
558
- return "Gemini";
522
+ /**
523
+ * Add a pending request to the agent state.
524
+ */
525
+ addPendingRequestToState(toolCallId, toolName, input) {
526
+ this.session.updateAgentState((currentState) => ({
527
+ ...currentState,
528
+ requests: {
529
+ ...currentState.requests,
530
+ [toolCallId]: {
531
+ tool: toolName,
532
+ arguments: input,
533
+ createdAt: Date.now()
534
+ }
535
+ }
536
+ }));
559
537
  }
560
- return normalizeText(providerLabel) ?? "Agent";
561
- }
562
- function buildNotificationTitle(sessionLabel, providerLabel, actionLabel) {
563
- const providerDisplayLabel = deriveNotificationProviderLabel(providerLabel);
564
- const suffix = ` \xB7 ${providerDisplayLabel} \xB7 ${actionLabel}`;
565
- const maxSessionLength = Math.max(12, TITLE_MAX_LENGTH - suffix.length);
566
- return `${truncateText(sessionLabel, maxSessionLength)}${suffix}`;
567
- }
568
- function derivePathLabel(path) {
569
- const normalized = normalizeText(path);
570
- if (!normalized) {
571
- return null;
538
+ registerPendingRequest(toolCallId, toolName, input, logSuffix = "") {
539
+ return new Promise((resolve, reject) => {
540
+ const pending = {
541
+ resolve,
542
+ reject,
543
+ toolName,
544
+ input
545
+ };
546
+ pending.timeoutHandle = setTimeout(() => {
547
+ this.handlePendingRequestTimeout(toolCallId, pending);
548
+ }, getPendingInteractionTimeoutMs());
549
+ this.pendingRequests.set(toolCallId, pending);
550
+ this.addPendingRequestToState(toolCallId, toolName, input);
551
+ logger.debug(`${this.getLogPrefix()} Permission request sent for tool: ${toolName} (${toolCallId})${logSuffix}`);
552
+ });
572
553
  }
573
- const segments = normalized.split(/[\\/]+/).filter(Boolean);
574
- return segments[segments.length - 1] ?? normalized;
575
- }
576
- function deriveHomeLabel(homeSlug) {
577
- const normalized = normalizeText(homeSlug);
578
- if (!normalized) {
579
- return null;
554
+ hasPendingRequests() {
555
+ return this.pendingRequests.size > 0;
580
556
  }
581
- return normalized.replace(/[_-]+/g, " ");
582
- }
583
- function deriveNotificationSessionLabel(metadata, providerLabel) {
584
- const label = normalizeText(metadata?.name) ?? normalizeText(metadata?.summary?.text) ?? deriveHomeLabel(metadata?.happyOrg?.specialistHome?.homeSlug) ?? derivePathLabel(metadata?.path) ?? normalizeText(providerLabel) ?? "Session";
585
- return truncateText(label, SESSION_LABEL_MAX_LENGTH);
586
- }
587
- function deriveNotificationSummary(candidates, fallback, sessionLabel) {
588
- for (const candidate of candidates) {
589
- const normalized = normalizeText(candidate);
590
- if (!normalized) {
591
- continue;
557
+ supersedePendingRequests(reason = INTERACTION_SUPERSEDED_ERROR) {
558
+ const pendingSnapshot = Array.from(this.pendingRequests.entries());
559
+ if (pendingSnapshot.length === 0) {
560
+ return 0;
592
561
  }
593
- const truncated = truncateText(normalized, BODY_MAX_LENGTH);
594
- if (truncated.toLowerCase() !== sessionLabel.toLowerCase()) {
595
- return truncated;
562
+ this.pendingRequests.clear();
563
+ const completedAt = Date.now();
564
+ for (const [, pending] of pendingSnapshot) {
565
+ this.clearPendingRequestTimeout(pending);
566
+ pending.resolve({ decision: "abort" });
596
567
  }
597
- }
598
- return truncateText(fallback, BODY_MAX_LENGTH);
599
- }
600
- function cleanExtraData(extraData) {
601
- return Object.fromEntries(
602
- Object.entries(extraData).filter(([, value]) => {
603
- if (value === null || value === void 0) {
604
- return false;
605
- }
606
- if (typeof value === "string") {
607
- return value.trim().length > 0;
608
- }
609
- return true;
610
- })
611
- );
612
- }
613
- function buildRouteData(opts) {
614
- const taskContext = opts.metadata?.happyOrg?.taskContext ?? null;
615
- const routeTaskId = opts.report?.taskId ?? taskContext?.taskId ?? null;
616
- const routePositionId = opts.report?.positionId ?? taskContext?.positionId ?? null;
617
- const routeSessionId = normalizeText(opts.sessionId);
618
- const data = {
619
- notificationKind: opts.notificationKind,
620
- provider: toProviderKey(opts.providerLabel)
621
- };
622
- if (opts.routePreference === "session" && routeSessionId) {
623
- data.sessionId = routeSessionId;
624
- } else if (routeTaskId) {
625
- data.taskId = routeTaskId;
626
- } else if (routeSessionId) {
627
- data.sessionId = routeSessionId;
628
- } else if (routePositionId) {
629
- data.positionId = routePositionId;
630
- }
631
- Object.assign(data, cleanExtraData({
632
- contextTaskId: data.taskId ? null : routeTaskId,
633
- contextPositionId: data.positionId ? null : routePositionId,
634
- memberAgentId: opts.report?.memberAgentId ?? taskContext?.memberAgentId ?? null,
635
- supervisorAgentId: opts.report?.supervisorAgentId ?? taskContext?.supervisorAgentId ?? null
636
- }));
637
- if (opts.extraData) {
638
- Object.assign(data, cleanExtraData(opts.extraData));
639
- }
640
- return data;
641
- }
642
- function humanizeToolName(toolName) {
643
- const normalized = normalizeText(toolName);
644
- if (!normalized) {
645
- return "continue";
646
- }
647
- const replacements = {
648
- bash: "run a command",
649
- execute: "run a command",
650
- read: "read a file",
651
- write: "write a file",
652
- edit: "edit a file",
653
- multiedit: "edit files",
654
- patch: "apply a patch",
655
- fetch: "fetch a page",
656
- search: "search the web"
657
- };
658
- const replacement = replacements[normalized.toLowerCase()];
659
- if (replacement) {
660
- return replacement;
661
- }
662
- return normalized.replace(/[_-]+/g, " ");
663
- }
664
- function extractPermissionRequestPushContext(message) {
665
- const payload = isRecord$1(message.payload) ? message.payload : null;
666
- const toolName = normalizeText(payload?.toolName) ?? normalizeText(message.reason);
667
- const payloadDescription = normalizeText(payload?.description);
668
- const reasonDescription = normalizeText(message.reason);
669
- let description = payloadDescription;
670
- if (!description && reasonDescription && reasonDescription.toLowerCase() !== (toolName ?? "").toLowerCase()) {
671
- description = reasonDescription;
672
- }
673
- if (description && toolName && description.toLowerCase() === toolName.toLowerCase()) {
674
- description = null;
675
- }
676
- return {
677
- requestId: message.id,
678
- toolName,
679
- description
680
- };
681
- }
682
- function buildReadyPushNotification(opts) {
683
- const sessionLabel = deriveNotificationSessionLabel(opts.metadata, opts.providerLabel);
684
- const providerDisplayLabel = deriveNotificationProviderLabel(opts.providerLabel);
685
- return {
686
- title: buildNotificationTitle(sessionLabel, opts.providerLabel, "Ready"),
687
- body: truncateText(`${providerDisplayLabel} is waiting for your next message`, BODY_MAX_LENGTH),
688
- data: buildRouteData({
689
- ...opts,
690
- notificationKind: "ready",
691
- routePreference: "session"
692
- })
693
- };
694
- }
695
- function buildPermissionPushNotification(opts) {
696
- const sessionLabel = deriveNotificationSessionLabel(opts.metadata, opts.providerLabel);
697
- const providerDisplayLabel = deriveNotificationProviderLabel(opts.providerLabel);
698
- const fallback = `${providerDisplayLabel} needs your approval to ${humanizeToolName(opts.toolName)}`;
699
- const body = deriveNotificationSummary([opts.description], fallback, sessionLabel);
700
- return {
701
- title: buildNotificationTitle(sessionLabel, opts.providerLabel, "Permission needed"),
702
- body,
703
- data: buildRouteData({
704
- ...opts,
705
- notificationKind: "permission_request",
706
- routePreference: "session",
707
- extraData: {
708
- requestId: opts.requestId,
709
- tool: opts.toolName ?? null,
710
- ...opts.extraData
568
+ this.session.updateAgentState((currentState) => {
569
+ const requests = { ...currentState.requests || {} };
570
+ const completedRequests = { ...currentState.completedRequests || {} };
571
+ for (const [id, request] of Object.entries(requests)) {
572
+ if (request.requestKind === "selection") {
573
+ continue;
574
+ }
575
+ completedRequests[id] = {
576
+ ...request,
577
+ completedAt,
578
+ status: "denied",
579
+ reason,
580
+ decision: "abort",
581
+ requestKind: request.requestKind || "permission"
582
+ };
583
+ delete requests[id];
711
584
  }
712
- })
713
- };
714
- }
715
- function buildTurnResultPushNotification(opts) {
716
- const sessionLabel = deriveNotificationSessionLabel(opts.metadata, opts.providerLabel);
717
- const providerDisplayLabel = deriveNotificationProviderLabel(opts.providerLabel);
718
- const report = opts.report ?? null;
719
- const explicitSummary = normalizeText(report?.summary) ?? normalizeText(opts.responseText);
720
- if (!explicitSummary) {
721
- return null;
585
+ return {
586
+ ...currentState,
587
+ requests,
588
+ completedRequests
589
+ };
590
+ });
591
+ logger.debug(`${this.getLogPrefix()} Superseded ${pendingSnapshot.length} pending permission request(s)`);
592
+ return pendingSnapshot.length;
722
593
  }
723
- const fallback = `${providerDisplayLabel} has a new result ready`;
724
- const body = deriveNotificationSummary(
725
- [
726
- explicitSummary
727
- ],
728
- fallback,
729
- sessionLabel
730
- );
731
- const action = report?.turnStatus === "task_complete" ? "Task complete" : "New result";
732
- return {
733
- title: buildNotificationTitle(sessionLabel, opts.providerLabel, action),
734
- body,
735
- data: buildRouteData({
736
- ...opts,
737
- report,
738
- notificationKind: "turn_result",
739
- routePreference: "session",
740
- extraData: {
741
- turnStatus: report?.turnStatus ?? "turn_update"
594
+ /**
595
+ * Reset state for new sessions.
596
+ * This method is idempotent - safe to call multiple times.
597
+ */
598
+ reset() {
599
+ if (this.isResetting) {
600
+ logger.debug(`${this.getLogPrefix()} Reset already in progress, skipping`);
601
+ return;
602
+ }
603
+ this.isResetting = true;
604
+ try {
605
+ const pendingSnapshot = Array.from(this.pendingRequests.entries());
606
+ this.pendingRequests.clear();
607
+ for (const [id, pending] of pendingSnapshot) {
608
+ try {
609
+ this.clearPendingRequestTimeout(pending);
610
+ pending.reject(new Error("Session reset"));
611
+ } catch (err) {
612
+ logger.debug(`${this.getLogPrefix()} Error rejecting pending request ${id}:`, err);
613
+ }
742
614
  }
743
- })
744
- };
745
- }
746
-
747
- const STRUCTURED_FIELD_PATTERN = /^\s*([a-z_]+)\s*=\s*(.*?)\s*$/;
748
- const KEY_VALUE_LINE_PATTERN = /^([A-Za-z0-9_]+)=(.*)$/;
749
- const HAPPY_ORG_READ_ACK = "yes";
750
- const DISPATCH_ID_ALIASES = ["dispatch_id", "dispatchId"];
751
- const ACK_VERSION_ALIASES = ["ack_version", "ackVersion"];
752
- const ORGANIZATION_ID_ALIASES = ["organization_id", "organizationId"];
753
- const TASK_ID_ALIASES = ["task_id", "taskId"];
754
- const MEMBER_AGENT_ID_ALIASES = ["member_agent_id", "memberAgentId", "agent_id", "agentId"];
755
- const SESSION_ID_ALIASES = ["session_id", "sessionId"];
756
- const POSITION_ID_ALIASES = ["position_id", "positionId"];
757
- const RESPONSIBILITY_ID_ALIASES = ["responsibility_id", "responsibilityId"];
758
- const ROUTE_TYPE_ALIASES = ["route_type", "routeType"];
759
- const ACK_TYPE_ALIASES = ["ack_type", "ackType"];
760
- const REPLY_MODE_ALIASES = ["reply_mode", "replyMode"];
761
- const PLAN_INTENT_ALIASES = ["plan_intent", "planIntent"];
762
- const ROUTER_REASON_ALIASES = ["router_reason", "routerReason"];
763
- const GOLDEN_ROUTE_ID_ALIASES = ["golden_route_id", "goldenRouteId"];
764
- const TASK_ACK_ALIASES = ["task_ack", "taskAck", "task_id", "taskId"];
765
- const SCOPE_ALIASES = ["scope"];
766
- const READ_ACK_ALIASES = ["read_ack", "readAck"];
767
- const STATUS_ALIASES = ["status"];
768
- const NOTE_ALIASES = ["note"];
769
- const REPLY_TO_ALIASES = ["reply_to", "replyTo"];
770
- const POSITION_STATUS_ALIASES = ["position_status", "positionStatus"];
771
- const LATEST_USER_VISIBLE_RESULT_ALIASES = ["latest_user_visible_result", "latestUserVisibleResult"];
772
- const BLOCKER_SUMMARY_ALIASES = ["blocker_summary", "blockerSummary"];
773
- const ACCEPTANCE_STATE_ALIASES = ["acceptance_state", "acceptanceState"];
774
- const CEO_WRITE_NEXT_STEP_ALIASES = ["ceo_write_next_step", "ceoWriteNextStep"];
775
- function normalizeOptionalText(value) {
776
- if (typeof value !== "string") {
777
- return null;
615
+ this.session.updateAgentState((currentState) => {
616
+ const pendingRequests = currentState.requests || {};
617
+ const completedRequests = { ...currentState.completedRequests };
618
+ for (const [id, request] of Object.entries(pendingRequests)) {
619
+ completedRequests[id] = {
620
+ ...request,
621
+ completedAt: Date.now(),
622
+ status: "canceled",
623
+ reason: "Session reset"
624
+ };
625
+ }
626
+ return {
627
+ ...currentState,
628
+ requests: {},
629
+ completedRequests
630
+ };
631
+ });
632
+ logger.debug(`${this.getLogPrefix()} Permission handler reset`);
633
+ } finally {
634
+ this.isResetting = false;
635
+ }
778
636
  }
779
- const trimmed = value.trim();
780
- return trimmed.length > 0 ? trimmed : null;
781
- }
782
- function normalizeAccessChannelState(value) {
783
- const normalized = normalizeSingleLineText(value)?.toLowerCase();
784
- if (normalized === "ok" || normalized === "reattach_required" || normalized === "runtime_replaced") {
785
- return normalized;
637
+ clearPendingRequestTimeout(pending) {
638
+ if (pending?.timeoutHandle) {
639
+ clearTimeout(pending.timeoutHandle);
640
+ pending.timeoutHandle = void 0;
641
+ }
786
642
  }
787
- return null;
788
- }
789
- function normalizeSummaryText(value) {
790
- const normalized = normalizeOptionalText(value);
791
- return normalized ? normalized.replace(/\s+/g, " ").slice(0, HAPPY_ORG_SUMMARY_MAX_LENGTH) : null;
792
- }
793
- function normalizeSingleLineText(value) {
794
- const normalized = normalizeOptionalText(value);
795
- if (!normalized || normalized.includes("\n") || normalized.includes("\r")) {
796
- return null;
643
+ handlePendingRequestTimeout(toolCallId, pending) {
644
+ const active = this.pendingRequests.get(toolCallId);
645
+ if (!active || active !== pending) {
646
+ return;
647
+ }
648
+ this.pendingRequests.delete(toolCallId);
649
+ this.clearPendingRequestTimeout(active);
650
+ active.resolve({ decision: "abort" });
651
+ this.session.updateAgentState((currentState) => {
652
+ const request = currentState.requests?.[toolCallId] || {
653
+ tool: active.toolName,
654
+ arguments: active.input,
655
+ createdAt: Date.now(),
656
+ requestKind: "permission"
657
+ };
658
+ const { [toolCallId]: _, ...remainingRequests } = currentState.requests || {};
659
+ return {
660
+ ...currentState,
661
+ requests: remainingRequests,
662
+ completedRequests: {
663
+ ...currentState.completedRequests,
664
+ [toolCallId]: {
665
+ ...request,
666
+ completedAt: Date.now(),
667
+ status: "canceled",
668
+ reason: INTERACTION_TIMED_OUT_ERROR,
669
+ decision: "abort",
670
+ requestKind: request.requestKind || "permission"
671
+ }
672
+ }
673
+ };
674
+ });
675
+ this.session.sendSessionEvent({
676
+ type: "message",
677
+ message: "Pending interaction timed out waiting for a response. Send a new message to continue."
678
+ });
679
+ logger.debug(`${this.getLogPrefix()} Permission request timed out for ${active.toolName} (${toolCallId})`);
797
680
  }
798
- return normalized;
799
- }
800
- function cloneHappyOrgReplyContext(replyContext) {
801
- return replyContext ? { ...replyContext } : null;
802
- }
803
- function cloneHappyOrgSpecialistHomeIdentity(specialistHome) {
804
- return specialistHome ? { ...specialistHome } : null;
805
681
  }
806
- function cloneHappyOrgAcceptanceHandoff(handoff) {
807
- if (handoff === void 0) {
808
- return void 0;
682
+
683
+ class MessageQueue2 {
684
+ queue = [];
685
+ // Made public for testing
686
+ waiter = null;
687
+ closed = false;
688
+ onMessageHandler = null;
689
+ modeHasher;
690
+ constructor(modeHasher, onMessageHandler = null) {
691
+ this.modeHasher = modeHasher;
692
+ this.onMessageHandler = onMessageHandler;
693
+ logger.debug(`[MessageQueue2] Initialized`);
809
694
  }
810
- return handoff ? { ...handoff } : null;
811
- }
812
- function cloneHappyOrgTurnReport(report) {
813
- if (!report) {
814
- return void 0;
695
+ /**
696
+ * Set a handler that will be called when a message arrives
697
+ */
698
+ setOnMessage(handler) {
699
+ this.onMessageHandler = handler;
815
700
  }
816
- const acceptanceHandoff = cloneHappyOrgAcceptanceHandoff(report.acceptanceHandoff);
817
- return {
818
- ...report,
819
- replyContext: cloneHappyOrgReplyContext(report.replyContext),
820
- specialistHome: cloneHappyOrgSpecialistHomeIdentity(report.specialistHome),
821
- ...acceptanceHandoff !== void 0 ? { acceptanceHandoff } : {}
822
- };
823
- }
824
- function cloneHappyOrgMetadata(happyOrg) {
825
- return {
826
- taskContext: happyOrg?.taskContext ? { ...happyOrg.taskContext } : void 0,
827
- runtime: happyOrg?.runtime ? { ...happyOrg.runtime } : void 0,
828
- activeOwner: happyOrg?.activeOwner ? { ...happyOrg.activeOwner } : null,
829
- replyContext: cloneHappyOrgReplyContext(happyOrg?.replyContext),
830
- dispatchAcks: happyOrg?.dispatchAcks ? Object.fromEntries(
831
- Object.entries(happyOrg.dispatchAcks).map(([dispatchId, entry]) => [
832
- dispatchId,
833
- { ...entry }
834
- ])
835
- ) : void 0,
836
- specialistHome: cloneHappyOrgSpecialistHomeIdentity(happyOrg?.specialistHome),
837
- repeat: happyOrg?.repeat ? {
838
- threshold: happyOrg.repeat.threshold,
839
- fingerprints: Object.fromEntries(
840
- Object.entries(happyOrg.repeat.fingerprints ?? {}).map(([fingerprint, entry]) => [
841
- fingerprint,
842
- { ...entry }
843
- ])
844
- )
845
- } : void 0,
846
- lastTurnReport: cloneHappyOrgTurnReport(happyOrg?.lastTurnReport)
847
- };
848
- }
849
- function normalizeHappyOrgMetadata(metadata) {
850
- const happyOrg = cloneHappyOrgMetadata(metadata?.happyOrg);
851
- return {
852
- ...happyOrg,
853
- runtime: happyOrg.runtime ?? {
854
- status: "active",
855
- reason: null
856
- },
857
- dispatchAcks: happyOrg.dispatchAcks ?? {},
858
- repeat: happyOrg.repeat ?? {
859
- threshold: HAPPY_ORG_REPEAT_THRESHOLD,
860
- fingerprints: {}
861
- }
862
- };
863
- }
864
- function withHappyOrgMetadata(metadata, happyOrg) {
865
- return {
866
- ...metadata,
867
- happyOrg
868
- };
869
- }
870
- function resetHappyOrgRuntimeForTask(taskContext, specialistHome) {
871
- return {
872
- taskContext,
873
- runtime: {
874
- status: "active",
875
- reason: null
876
- },
877
- activeOwner: null,
878
- replyContext: null,
879
- dispatchAcks: {},
880
- specialistHome: cloneHappyOrgSpecialistHomeIdentity(specialistHome),
881
- repeat: {
882
- threshold: HAPPY_ORG_REPEAT_THRESHOLD,
883
- fingerprints: {}
701
+ /**
702
+ * Push a message to the queue with a mode.
703
+ */
704
+ push(message, mode) {
705
+ if (this.closed) {
706
+ throw new Error("Cannot push to closed queue");
884
707
  }
885
- };
886
- }
887
- function parseStructuredFields(text) {
888
- const fields = {};
889
- for (const rawLine of text.split(/\r?\n/)) {
890
- const match = rawLine.match(STRUCTURED_FIELD_PATTERN);
891
- if (!match) {
892
- continue;
708
+ const modeHash = this.modeHasher(mode);
709
+ logger.debug(`[MessageQueue2] push() called with mode hash: ${modeHash}`);
710
+ this.queue.push({
711
+ message,
712
+ mode,
713
+ modeHash,
714
+ isolate: false
715
+ });
716
+ if (this.onMessageHandler) {
717
+ this.onMessageHandler(message, mode);
893
718
  }
894
- const [, key, value] = match;
895
- if (!key) {
896
- continue;
719
+ if (this.waiter) {
720
+ logger.debug(`[MessageQueue2] Notifying waiter`);
721
+ const waiter = this.waiter;
722
+ this.waiter = null;
723
+ waiter(true);
897
724
  }
898
- fields[key] = value.trim();
899
- }
900
- return fields;
901
- }
902
- function parseJsonEnvelopeFields(text) {
903
- const trimmed = text.trim();
904
- if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
905
- return null;
725
+ logger.debug(`[MessageQueue2] push() completed. Queue size: ${this.queue.length}`);
906
726
  }
907
- try {
908
- const parsed = JSON.parse(trimmed);
909
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
910
- return null;
911
- }
912
- const record = parsed;
913
- const fields = {};
914
- for (const key of [
915
- "ack_version",
916
- "dispatch_id",
917
- "organization_id",
918
- "task_id",
919
- "scope",
920
- "member_agent_id",
921
- "agent_id",
922
- "session_id",
923
- "position_id",
924
- "responsibility_id",
925
- "reply_to",
926
- "route_type",
927
- "ack_type",
928
- "reply_mode",
929
- "plan_intent",
930
- "router_reason",
931
- "golden_route_id",
932
- "task_ack",
933
- "read_ack",
934
- "status",
935
- "note"
936
- ]) {
937
- const value = normalizeOptionalText(record[key]);
938
- if (value) {
939
- fields[key] = value;
940
- }
727
+ /**
728
+ * Push a message immediately without batching delay.
729
+ * Does not clear the queue or enforce isolation.
730
+ */
731
+ pushImmediate(message, mode) {
732
+ if (this.closed) {
733
+ throw new Error("Cannot push to closed queue");
941
734
  }
942
- return fields;
943
- } catch {
944
- return null;
945
- }
946
- }
947
- function parseHappyOrgEnvelopeFields(text) {
948
- return parseJsonEnvelopeFields(text) ?? parseStructuredFields(text);
949
- }
950
- function getFirstDefined(record, aliases) {
951
- for (const alias of aliases) {
952
- if (Object.prototype.hasOwnProperty.call(record, alias)) {
953
- return record[alias];
735
+ const modeHash = this.modeHasher(mode);
736
+ logger.debug(`[MessageQueue2] pushImmediate() called with mode hash: ${modeHash}`);
737
+ this.queue.push({
738
+ message,
739
+ mode,
740
+ modeHash,
741
+ isolate: false
742
+ });
743
+ if (this.onMessageHandler) {
744
+ this.onMessageHandler(message, mode);
954
745
  }
746
+ if (this.waiter) {
747
+ logger.debug(`[MessageQueue2] Notifying waiter for immediate message`);
748
+ const waiter = this.waiter;
749
+ this.waiter = null;
750
+ waiter(true);
751
+ }
752
+ logger.debug(`[MessageQueue2] pushImmediate() completed. Queue size: ${this.queue.length}`);
955
753
  }
956
- return void 0;
957
- }
958
- function normalizeDispatchAckStatus(value) {
959
- const normalized = normalizeSingleLineText(value)?.toLowerCase();
960
- if (normalized === "accepted" || normalized === "standby" || normalized === "blocked") {
961
- return normalized;
962
- }
963
- return null;
964
- }
965
- function normalizeDispatchReadAck(value) {
966
- if (value === true) {
967
- return HAPPY_ORG_READ_ACK;
968
- }
969
- const normalized = normalizeSingleLineText(value)?.toLowerCase();
970
- if (!normalized) {
971
- return null;
754
+ /**
755
+ * Push a message that must be processed in complete isolation.
756
+ * Clears any pending messages and ensures this message is never batched with others.
757
+ * Used for special commands that require dedicated processing.
758
+ */
759
+ pushIsolateAndClear(message, mode) {
760
+ if (this.closed) {
761
+ throw new Error("Cannot push to closed queue");
762
+ }
763
+ const modeHash = this.modeHasher(mode);
764
+ logger.debug(`[MessageQueue2] pushIsolateAndClear() called with mode hash: ${modeHash} - clearing ${this.queue.length} pending messages`);
765
+ this.queue = [];
766
+ this.queue.push({
767
+ message,
768
+ mode,
769
+ modeHash,
770
+ isolate: true
771
+ });
772
+ if (this.onMessageHandler) {
773
+ this.onMessageHandler(message, mode);
774
+ }
775
+ if (this.waiter) {
776
+ logger.debug(`[MessageQueue2] Notifying waiter for isolated message`);
777
+ const waiter = this.waiter;
778
+ this.waiter = null;
779
+ waiter(true);
780
+ }
781
+ logger.debug(`[MessageQueue2] pushIsolateAndClear() completed. Queue size: ${this.queue.length}`);
972
782
  }
973
- return normalized === "yes" || normalized === "true" ? HAPPY_ORG_READ_ACK : null;
974
- }
975
- function normalizeRouteType(value) {
976
- const normalized = normalizeSingleLineText(value)?.toLowerCase();
977
- if (normalized === "direct_reply" || normalized === "version_planning_reply" || normalized === "analysis_task" || normalized === "implementation_task" || normalized === "ack_plus_reply") {
978
- return normalized;
783
+ /**
784
+ * Push a message to the beginning of the queue with a mode.
785
+ */
786
+ unshift(message, mode) {
787
+ if (this.closed) {
788
+ throw new Error("Cannot unshift to closed queue");
789
+ }
790
+ const modeHash = this.modeHasher(mode);
791
+ logger.debug(`[MessageQueue2] unshift() called with mode hash: ${modeHash}`);
792
+ this.queue.unshift({
793
+ message,
794
+ mode,
795
+ modeHash,
796
+ isolate: false
797
+ });
798
+ if (this.onMessageHandler) {
799
+ this.onMessageHandler(message, mode);
800
+ }
801
+ if (this.waiter) {
802
+ logger.debug(`[MessageQueue2] Notifying waiter`);
803
+ const waiter = this.waiter;
804
+ this.waiter = null;
805
+ waiter(true);
806
+ }
807
+ logger.debug(`[MessageQueue2] unshift() completed. Queue size: ${this.queue.length}`);
979
808
  }
980
- return null;
981
- }
982
- function normalizeAckType(value) {
983
- const normalized = normalizeSingleLineText(value)?.toLowerCase();
984
- if (normalized === "none" || normalized === "read_ack" || normalized === "dispatch_ack" || normalized === "route_ack" || normalized === "progress_ack") {
985
- return normalized;
809
+ /**
810
+ * Reset the queue - clears all messages and resets to empty state
811
+ */
812
+ reset() {
813
+ logger.debug(`[MessageQueue2] reset() called. Clearing ${this.queue.length} messages`);
814
+ this.queue = [];
815
+ this.closed = false;
816
+ this.waiter = null;
986
817
  }
987
- return null;
988
- }
989
- function normalizeReplyMode(value) {
990
- const normalized = normalizeSingleLineText(value)?.toLowerCase();
991
- if (normalized === "reply-first" || normalized === "analysis-first") {
992
- return normalized;
818
+ /**
819
+ * Close the queue - no more messages can be pushed
820
+ */
821
+ close() {
822
+ logger.debug(`[MessageQueue2] close() called`);
823
+ this.closed = true;
824
+ if (this.waiter) {
825
+ const waiter = this.waiter;
826
+ this.waiter = null;
827
+ waiter(false);
828
+ }
993
829
  }
994
- return null;
995
- }
996
- function normalizeAcceptanceState(value) {
997
- const normalized = normalizeSingleLineText(value)?.toLowerCase();
998
- if (normalized === "not_ready" || normalized === "awaiting_acceptance" || normalized === "closed") {
999
- return normalized;
830
+ /**
831
+ * Check if the queue is closed
832
+ */
833
+ isClosed() {
834
+ return this.closed;
1000
835
  }
1001
- return null;
1002
- }
1003
- function normalizeBlockerSummary(value) {
1004
- const normalized = normalizeSingleLineText(value);
1005
- if (!normalized) {
1006
- return null;
836
+ /**
837
+ * Get the current queue size
838
+ */
839
+ size() {
840
+ return this.queue.length;
1007
841
  }
1008
- const lowered = normalized.toLowerCase();
1009
- if (lowered === "none" || lowered === "null" || lowered === "no blocker") {
1010
- return null;
842
+ /**
843
+ * Wait for messages and return all messages with the same mode as a single string
844
+ * Returns { message: string, mode: T } or null if aborted/closed
845
+ */
846
+ async waitForMessagesAndGetAsString(abortSignal) {
847
+ if (this.queue.length > 0) {
848
+ return this.collectBatch();
849
+ }
850
+ if (this.closed || abortSignal?.aborted) {
851
+ return null;
852
+ }
853
+ const hasMessages = await this.waitForMessages(abortSignal);
854
+ if (!hasMessages) {
855
+ return null;
856
+ }
857
+ return this.collectBatch();
1011
858
  }
1012
- return normalized;
1013
- }
1014
- function normalizeHappyOrgAcceptanceHandoffRecord(record) {
1015
- const dispatchId = normalizeSingleLineText(getFirstDefined(record, DISPATCH_ID_ALIASES));
1016
- const replyTo = normalizeSingleLineText(getFirstDefined(record, REPLY_TO_ALIASES));
1017
- const taskId = normalizeSingleLineText(getFirstDefined(record, TASK_ID_ALIASES));
1018
- const positionStatus = normalizeSingleLineText(getFirstDefined(record, POSITION_STATUS_ALIASES));
1019
- const latestUserVisibleResult = normalizeSingleLineText(getFirstDefined(record, LATEST_USER_VISIBLE_RESULT_ALIASES));
1020
- const acceptanceState = normalizeAcceptanceState(getFirstDefined(record, ACCEPTANCE_STATE_ALIASES));
1021
- const ceoWriteNextStep = normalizeSingleLineText(getFirstDefined(record, CEO_WRITE_NEXT_STEP_ALIASES));
1022
- if (!dispatchId || !replyTo || !taskId || !positionStatus || !latestUserVisibleResult || !acceptanceState || !ceoWriteNextStep) {
1023
- return null;
859
+ /**
860
+ * Collect a batch of messages with the same mode, respecting isolation requirements
861
+ */
862
+ collectBatch() {
863
+ if (this.queue.length === 0) {
864
+ return null;
865
+ }
866
+ const firstItem = this.queue[0];
867
+ const sameModeMessages = [];
868
+ let mode = firstItem.mode;
869
+ let isolate = firstItem.isolate ?? false;
870
+ const targetModeHash = firstItem.modeHash;
871
+ if (firstItem.isolate) {
872
+ const item = this.queue.shift();
873
+ sameModeMessages.push(item.message);
874
+ logger.debug(`[MessageQueue2] Collected isolated message with mode hash: ${targetModeHash}`);
875
+ } else {
876
+ while (this.queue.length > 0 && this.queue[0].modeHash === targetModeHash && !this.queue[0].isolate) {
877
+ const item = this.queue.shift();
878
+ sameModeMessages.push(item.message);
879
+ }
880
+ logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
881
+ }
882
+ const combinedMessage = sameModeMessages.join("\n");
883
+ return {
884
+ message: combinedMessage,
885
+ mode,
886
+ hash: targetModeHash,
887
+ isolate
888
+ };
889
+ }
890
+ /**
891
+ * Wait for messages to arrive
892
+ */
893
+ waitForMessages(abortSignal) {
894
+ return new Promise((resolve) => {
895
+ let abortHandler = null;
896
+ if (abortSignal) {
897
+ abortHandler = () => {
898
+ logger.debug("[MessageQueue2] Wait aborted");
899
+ if (this.waiter === waiterFunc) {
900
+ this.waiter = null;
901
+ }
902
+ resolve(false);
903
+ };
904
+ abortSignal.addEventListener("abort", abortHandler);
905
+ }
906
+ const waiterFunc = (hasMessages) => {
907
+ if (abortHandler && abortSignal) {
908
+ abortSignal.removeEventListener("abort", abortHandler);
909
+ }
910
+ resolve(hasMessages);
911
+ };
912
+ if (this.queue.length > 0) {
913
+ if (abortHandler && abortSignal) {
914
+ abortSignal.removeEventListener("abort", abortHandler);
915
+ }
916
+ resolve(true);
917
+ return;
918
+ }
919
+ if (this.closed || abortSignal?.aborted) {
920
+ if (abortHandler && abortSignal) {
921
+ abortSignal.removeEventListener("abort", abortHandler);
922
+ }
923
+ resolve(false);
924
+ return;
925
+ }
926
+ this.waiter = waiterFunc;
927
+ logger.debug("[MessageQueue2] Waiting for messages...");
928
+ });
1024
929
  }
1025
- return {
1026
- dispatchId,
1027
- replyTo,
1028
- taskId,
1029
- positionId: normalizeSingleLineText(getFirstDefined(record, POSITION_ID_ALIASES)),
1030
- responsibilityId: normalizeSingleLineText(getFirstDefined(record, RESPONSIBILITY_ID_ALIASES)),
1031
- positionStatus,
1032
- latestUserVisibleResult,
1033
- blockerSummary: normalizeBlockerSummary(getFirstDefined(record, BLOCKER_SUMMARY_ALIASES)),
1034
- acceptanceState,
1035
- ceoWriteNextStep
1036
- };
1037
930
  }
1038
- function normalizeHappyOrgDispatchAckRecord(record) {
1039
- const ackVersion = normalizeSingleLineText(getFirstDefined(record, ACK_VERSION_ALIASES));
1040
- const dispatchId = normalizeSingleLineText(getFirstDefined(record, DISPATCH_ID_ALIASES));
1041
- const organizationId = normalizeSingleLineText(getFirstDefined(record, ORGANIZATION_ID_ALIASES));
1042
- const taskId = normalizeSingleLineText(getFirstDefined(record, TASK_ID_ALIASES));
1043
- const scope = normalizeSingleLineText(getFirstDefined(record, SCOPE_ALIASES));
1044
- const memberAgentId = normalizeSingleLineText(getFirstDefined(record, MEMBER_AGENT_ID_ALIASES));
1045
- const sessionId = normalizeSingleLineText(getFirstDefined(record, SESSION_ID_ALIASES));
1046
- const positionId = normalizeSingleLineText(getFirstDefined(record, POSITION_ID_ALIASES));
1047
- const responsibilityId = normalizeSingleLineText(getFirstDefined(record, RESPONSIBILITY_ID_ALIASES));
1048
- const routeType = normalizeRouteType(getFirstDefined(record, ROUTE_TYPE_ALIASES));
1049
- const ackType = normalizeAckType(getFirstDefined(record, ACK_TYPE_ALIASES));
1050
- const replyMode = normalizeReplyMode(getFirstDefined(record, REPLY_MODE_ALIASES));
1051
- const planIntent = normalizeSingleLineText(getFirstDefined(record, PLAN_INTENT_ALIASES));
1052
- const routerReason = normalizeSingleLineText(getFirstDefined(record, ROUTER_REASON_ALIASES));
1053
- const goldenRouteId = normalizeSingleLineText(getFirstDefined(record, GOLDEN_ROUTE_ID_ALIASES));
1054
- const taskAck = normalizeSingleLineText(getFirstDefined(record, TASK_ACK_ALIASES));
1055
- const readAck = normalizeDispatchReadAck(getFirstDefined(record, READ_ACK_ALIASES));
1056
- const status = normalizeDispatchAckStatus(getFirstDefined(record, STATUS_ALIASES));
1057
- const note = normalizeSingleLineText(getFirstDefined(record, NOTE_ALIASES));
1058
- if (!dispatchId || !scope || !taskAck || !readAck || !status || !note) {
931
+
932
+ function registerKillSessionHandler(rpcHandlerManager, killThisHappy) {
933
+ rpcHandlerManager.registerHandler("killSession", async () => {
934
+ logger.debug("Kill session request received");
935
+ void killThisHappy();
936
+ return {
937
+ success: true,
938
+ message: "Killing happy-cli process"
939
+ };
940
+ });
941
+ }
942
+
943
+ const STRUCTURED_FIELD_PATTERN = /^\s*([a-z_]+)\s*=\s*(.*?)\s*$/;
944
+ const KEY_VALUE_LINE_PATTERN = /^([A-Za-z0-9_]+)=(.*)$/;
945
+ const HAPPY_ORG_READ_ACK = "yes";
946
+ const DISPATCH_ID_ALIASES = ["dispatch_id", "dispatchId"];
947
+ const ACK_VERSION_ALIASES = ["ack_version", "ackVersion"];
948
+ const ORGANIZATION_ID_ALIASES = ["organization_id", "organizationId"];
949
+ const TASK_ID_ALIASES = ["task_id", "taskId"];
950
+ const MEMBER_AGENT_ID_ALIASES = ["member_agent_id", "memberAgentId", "agent_id", "agentId"];
951
+ const SESSION_ID_ALIASES = ["session_id", "sessionId"];
952
+ const POSITION_ID_ALIASES = ["position_id", "positionId"];
953
+ const RESPONSIBILITY_ID_ALIASES = ["responsibility_id", "responsibilityId"];
954
+ const ROUTE_TYPE_ALIASES = ["route_type", "routeType"];
955
+ const ACK_TYPE_ALIASES = ["ack_type", "ackType"];
956
+ const REPLY_MODE_ALIASES = ["reply_mode", "replyMode"];
957
+ const PLAN_INTENT_ALIASES = ["plan_intent", "planIntent"];
958
+ const ROUTER_REASON_ALIASES = ["router_reason", "routerReason"];
959
+ const GOLDEN_ROUTE_ID_ALIASES = ["golden_route_id", "goldenRouteId"];
960
+ const TASK_ACK_ALIASES = ["task_ack", "taskAck", "task_id", "taskId"];
961
+ const SCOPE_ALIASES = ["scope"];
962
+ const READ_ACK_ALIASES = ["read_ack", "readAck"];
963
+ const STATUS_ALIASES = ["status"];
964
+ const NOTE_ALIASES = ["note"];
965
+ const REPLY_TO_ALIASES = ["reply_to", "replyTo"];
966
+ const POSITION_STATUS_ALIASES = ["position_status", "positionStatus"];
967
+ const LATEST_USER_VISIBLE_RESULT_ALIASES = ["latest_user_visible_result", "latestUserVisibleResult"];
968
+ const BLOCKER_SUMMARY_ALIASES = ["blocker_summary", "blockerSummary"];
969
+ const ACCEPTANCE_STATE_ALIASES = ["acceptance_state", "acceptanceState"];
970
+ const CEO_WRITE_NEXT_STEP_ALIASES = ["ceo_write_next_step", "ceoWriteNextStep"];
971
+ function normalizeOptionalText(value) {
972
+ if (typeof value !== "string") {
1059
973
  return null;
1060
974
  }
1061
- return {
1062
- ackVersion,
1063
- dispatchId,
1064
- organizationId,
1065
- taskId,
1066
- scope,
1067
- memberAgentId,
1068
- sessionId,
1069
- positionId,
1070
- responsibilityId,
1071
- routeType,
1072
- ackType,
1073
- replyMode,
1074
- planIntent,
1075
- routerReason,
1076
- goldenRouteId,
1077
- taskAck,
1078
- readAck,
1079
- status,
1080
- note
1081
- };
975
+ const trimmed = value.trim();
976
+ return trimmed.length > 0 ? trimmed : null;
1082
977
  }
1083
- function isHappyOrgDispatchAckCandidateRecord(record) {
1084
- return [
1085
- ...DISPATCH_ID_ALIASES,
1086
- ...ACK_VERSION_ALIASES,
1087
- ...ORGANIZATION_ID_ALIASES,
1088
- ...TASK_ID_ALIASES,
1089
- ...MEMBER_AGENT_ID_ALIASES,
1090
- ...SESSION_ID_ALIASES,
1091
- ...POSITION_ID_ALIASES,
1092
- ...RESPONSIBILITY_ID_ALIASES,
1093
- ...ROUTE_TYPE_ALIASES,
1094
- ...ACK_TYPE_ALIASES,
1095
- ...REPLY_MODE_ALIASES,
1096
- ...PLAN_INTENT_ALIASES,
1097
- ...ROUTER_REASON_ALIASES,
1098
- ...GOLDEN_ROUTE_ID_ALIASES,
1099
- ...TASK_ACK_ALIASES,
1100
- ...SCOPE_ALIASES,
1101
- ...READ_ACK_ALIASES,
1102
- ...STATUS_ALIASES,
1103
- ...NOTE_ALIASES
1104
- ].some((key) => Object.prototype.hasOwnProperty.call(record, key));
978
+ function normalizeAccessChannelState(value) {
979
+ const normalized = normalizeSingleLineText(value)?.toLowerCase();
980
+ if (normalized === "ok" || normalized === "reattach_required" || normalized === "runtime_replaced") {
981
+ return normalized;
982
+ }
983
+ return null;
1105
984
  }
1106
- function isHappyOrgAcceptanceHandoffCandidateRecord(record) {
1107
- return [
1108
- ...DISPATCH_ID_ALIASES,
1109
- ...REPLY_TO_ALIASES,
1110
- ...TASK_ID_ALIASES,
1111
- ...POSITION_ID_ALIASES,
1112
- ...RESPONSIBILITY_ID_ALIASES,
1113
- ...POSITION_STATUS_ALIASES,
1114
- ...LATEST_USER_VISIBLE_RESULT_ALIASES,
1115
- ...BLOCKER_SUMMARY_ALIASES,
1116
- ...ACCEPTANCE_STATE_ALIASES,
1117
- ...CEO_WRITE_NEXT_STEP_ALIASES
1118
- ].some((key) => Object.prototype.hasOwnProperty.call(record, key));
985
+ function normalizeSummaryText(value) {
986
+ const normalized = normalizeOptionalText(value);
987
+ return normalized ? normalized.replace(/\s+/g, " ").slice(0, HAPPY_ORG_SUMMARY_MAX_LENGTH) : null;
1119
988
  }
1120
- function extractKeyValueBlocks(text) {
1121
- const blocks = [];
1122
- let currentBlock = null;
1123
- for (const rawLine of text.split(/\r?\n/)) {
1124
- const line = rawLine.trim();
1125
- const match = KEY_VALUE_LINE_PATTERN.exec(line);
1126
- if (match) {
1127
- currentBlock ??= {};
1128
- currentBlock[match[1]] = match[2].trim();
1129
- continue;
1130
- }
1131
- if (currentBlock && Object.keys(currentBlock).length > 0) {
1132
- blocks.push(currentBlock);
1133
- currentBlock = null;
1134
- }
1135
- }
1136
- if (currentBlock && Object.keys(currentBlock).length > 0) {
1137
- blocks.push(currentBlock);
989
+ function normalizeSingleLineText(value) {
990
+ const normalized = normalizeOptionalText(value);
991
+ if (!normalized || normalized.includes("\n") || normalized.includes("\r")) {
992
+ return null;
1138
993
  }
1139
- return blocks;
994
+ return normalized;
1140
995
  }
1141
- function resolveHappyOrgDispatchAckEnvelopeCandidate(text) {
1142
- const trimmed = stripCodeFence(text).trim();
1143
- if (!trimmed) {
1144
- return { outcome: "absent" };
1145
- }
1146
- let sawCandidate = false;
1147
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
1148
- try {
1149
- const parsed = JSON.parse(trimmed);
1150
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1151
- const record = parsed;
1152
- if (isHappyOrgDispatchAckCandidateRecord(record)) {
1153
- const envelope = normalizeHappyOrgDispatchAckRecord(record);
1154
- if (envelope) {
1155
- return {
1156
- outcome: "valid",
1157
- envelope
1158
- };
1159
- }
1160
- sawCandidate = true;
1161
- }
1162
- }
1163
- } catch {
1164
- }
1165
- }
1166
- for (const block of extractKeyValueBlocks(text)) {
1167
- if (!isHappyOrgDispatchAckCandidateRecord(block)) {
1168
- continue;
1169
- }
1170
- const envelope = normalizeHappyOrgDispatchAckRecord(block);
1171
- if (envelope) {
1172
- return {
1173
- outcome: "valid",
1174
- envelope
1175
- };
1176
- }
1177
- sawCandidate = true;
1178
- }
1179
- return sawCandidate ? {
1180
- outcome: "invalid",
1181
- diagnostic: "Happy Org dispatch ack ignored: malformed envelope. Required fields are dispatch_id, scope, task_ack, read_ack=yes, status, and note."
1182
- } : {
1183
- outcome: "absent"
1184
- };
996
+ function cloneHappyOrgReplyContext(replyContext) {
997
+ return replyContext ? { ...replyContext } : null;
1185
998
  }
1186
- function resolveHappyOrgAcceptanceHandoffCandidate(text) {
1187
- const trimmed = stripCodeFence(text).trim();
1188
- if (!trimmed) {
1189
- return { outcome: "absent" };
999
+ function cloneHappyOrgSpecialistHomeIdentity(specialistHome) {
1000
+ return specialistHome ? { ...specialistHome } : null;
1001
+ }
1002
+ function cloneHappyOrgAcceptanceHandoff(handoff) {
1003
+ if (handoff === void 0) {
1004
+ return void 0;
1190
1005
  }
1191
- let sawCandidate = false;
1192
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
1193
- try {
1194
- const parsed = JSON.parse(trimmed);
1195
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1196
- const record = parsed;
1197
- if (isHappyOrgAcceptanceHandoffCandidateRecord(record)) {
1198
- const handoff = normalizeHappyOrgAcceptanceHandoffRecord(record);
1199
- if (handoff) {
1200
- return {
1201
- outcome: "valid",
1202
- handoff
1203
- };
1204
- }
1205
- sawCandidate = true;
1206
- }
1207
- }
1208
- } catch {
1209
- }
1006
+ return handoff ? { ...handoff } : null;
1007
+ }
1008
+ function cloneHappyOrgTurnReport(report) {
1009
+ if (!report) {
1010
+ return void 0;
1210
1011
  }
1211
- for (const block of extractKeyValueBlocks(text)) {
1212
- if (!isHappyOrgAcceptanceHandoffCandidateRecord(block)) {
1213
- continue;
1214
- }
1215
- const handoff = normalizeHappyOrgAcceptanceHandoffRecord(block);
1216
- if (handoff) {
1217
- return {
1218
- outcome: "valid",
1219
- handoff
1220
- };
1012
+ const acceptanceHandoff = cloneHappyOrgAcceptanceHandoff(report.acceptanceHandoff);
1013
+ return {
1014
+ ...report,
1015
+ replyContext: cloneHappyOrgReplyContext(report.replyContext),
1016
+ specialistHome: cloneHappyOrgSpecialistHomeIdentity(report.specialistHome),
1017
+ ...acceptanceHandoff !== void 0 ? { acceptanceHandoff } : {}
1018
+ };
1019
+ }
1020
+ function cloneHappyOrgMetadata(happyOrg) {
1021
+ return {
1022
+ taskContext: happyOrg?.taskContext ? { ...happyOrg.taskContext } : void 0,
1023
+ runtime: happyOrg?.runtime ? { ...happyOrg.runtime } : void 0,
1024
+ activeOwner: happyOrg?.activeOwner ? { ...happyOrg.activeOwner } : null,
1025
+ replyContext: cloneHappyOrgReplyContext(happyOrg?.replyContext),
1026
+ dispatchAcks: happyOrg?.dispatchAcks ? Object.fromEntries(
1027
+ Object.entries(happyOrg.dispatchAcks).map(([dispatchId, entry]) => [
1028
+ dispatchId,
1029
+ { ...entry }
1030
+ ])
1031
+ ) : void 0,
1032
+ specialistHome: cloneHappyOrgSpecialistHomeIdentity(happyOrg?.specialistHome),
1033
+ repeat: happyOrg?.repeat ? {
1034
+ threshold: happyOrg.repeat.threshold,
1035
+ fingerprints: Object.fromEntries(
1036
+ Object.entries(happyOrg.repeat.fingerprints ?? {}).map(([fingerprint, entry]) => [
1037
+ fingerprint,
1038
+ { ...entry }
1039
+ ])
1040
+ )
1041
+ } : void 0,
1042
+ lastTurnReport: cloneHappyOrgTurnReport(happyOrg?.lastTurnReport)
1043
+ };
1044
+ }
1045
+ function normalizeHappyOrgMetadata(metadata) {
1046
+ const happyOrg = cloneHappyOrgMetadata(metadata?.happyOrg);
1047
+ return {
1048
+ ...happyOrg,
1049
+ runtime: happyOrg.runtime ?? {
1050
+ status: "active",
1051
+ reason: null
1052
+ },
1053
+ dispatchAcks: happyOrg.dispatchAcks ?? {},
1054
+ repeat: happyOrg.repeat ?? {
1055
+ threshold: HAPPY_ORG_REPEAT_THRESHOLD,
1056
+ fingerprints: {}
1221
1057
  }
1222
- sawCandidate = true;
1223
- }
1224
- return sawCandidate ? {
1225
- outcome: "invalid",
1226
- diagnostic: "Happy Org acceptance handoff ignored: malformed mini-pack. Required fields are dispatch_id, reply_to, task_id, position_status, latest_user_visible_result, acceptance_state, and ceo_write_next_step."
1227
- } : {
1228
- outcome: "absent"
1229
1058
  };
1230
1059
  }
1231
- function recordHappyOrgDispatchAck(metadata, ack, acknowledgedAt) {
1232
- const nextHappyOrg = normalizeHappyOrgMetadata(metadata);
1233
- nextHappyOrg.dispatchAcks = nextHappyOrg.dispatchAcks ?? {};
1234
- nextHappyOrg.dispatchAcks[ack.dispatchId] = {
1235
- dispatchId: ack.dispatchId,
1236
- ackVersion: ack.ackVersion,
1237
- scope: ack.scope,
1238
- taskAck: ack.taskAck,
1239
- readAck: ack.readAck,
1240
- status: ack.status,
1241
- note: ack.note,
1242
- taskId: ack.taskId,
1243
- organizationId: ack.organizationId,
1244
- memberAgentId: ack.memberAgentId,
1245
- sessionId: ack.sessionId,
1246
- positionId: ack.positionId,
1247
- responsibilityId: ack.responsibilityId,
1248
- routeType: ack.routeType,
1249
- ackType: ack.ackType,
1250
- replyMode: ack.replyMode,
1251
- planIntent: ack.planIntent,
1252
- routerReason: ack.routerReason,
1253
- goldenRouteId: ack.goldenRouteId,
1254
- acknowledgedAt
1060
+ function withHappyOrgMetadata(metadata, happyOrg) {
1061
+ return {
1062
+ ...metadata,
1063
+ happyOrg
1255
1064
  };
1256
- return withHappyOrgMetadata(metadata, nextHappyOrg);
1257
1065
  }
1258
- function normalizeDispatchAckError(error) {
1259
- if (error instanceof Error) {
1260
- return error.message.trim() || "Unknown error";
1261
- }
1262
- if (typeof error === "string") {
1263
- return error.trim() || "Unknown error";
1066
+ function resetHappyOrgRuntimeForTask(taskContext, specialistHome) {
1067
+ return {
1068
+ taskContext,
1069
+ runtime: {
1070
+ status: "active",
1071
+ reason: null
1072
+ },
1073
+ activeOwner: null,
1074
+ replyContext: null,
1075
+ dispatchAcks: {},
1076
+ specialistHome: cloneHappyOrgSpecialistHomeIdentity(specialistHome),
1077
+ repeat: {
1078
+ threshold: HAPPY_ORG_REPEAT_THRESHOLD,
1079
+ fingerprints: {}
1080
+ }
1081
+ };
1082
+ }
1083
+ function parseStructuredFields(text) {
1084
+ const fields = {};
1085
+ for (const rawLine of text.split(/\r?\n/)) {
1086
+ const match = rawLine.match(STRUCTURED_FIELD_PATTERN);
1087
+ if (!match) {
1088
+ continue;
1089
+ }
1090
+ const [, key, value] = match;
1091
+ if (!key) {
1092
+ continue;
1093
+ }
1094
+ fields[key] = value.trim();
1264
1095
  }
1265
- return "Unknown error";
1096
+ return fields;
1266
1097
  }
1267
- async function maybeSubmitHappyOrgDispatchBusinessAck(opts) {
1268
- const metadata = opts.metadata ?? null;
1269
- const candidate = resolveHappyOrgDispatchAckEnvelopeCandidate(opts.responseText);
1270
- if (candidate.outcome === "absent") {
1271
- return {
1272
- nextMetadata: metadata,
1273
- dispatchAck: {
1274
- outcome: "none",
1275
- reason: "no_envelope",
1276
- diagnostic: null
1277
- }
1278
- };
1098
+ function parseJsonEnvelopeFields(text) {
1099
+ const trimmed = text.trim();
1100
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
1101
+ return null;
1279
1102
  }
1280
- if (candidate.outcome === "invalid") {
1281
- logger.debug("[HappyOrg] Dispatch ack candidate rejected as malformed");
1282
- return {
1283
- nextMetadata: metadata,
1284
- dispatchAck: {
1285
- outcome: "invalid",
1286
- reason: "malformed_envelope",
1287
- diagnostic: candidate.diagnostic
1103
+ try {
1104
+ const parsed = JSON.parse(trimmed);
1105
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1106
+ return null;
1107
+ }
1108
+ const record = parsed;
1109
+ const fields = {};
1110
+ for (const key of [
1111
+ "ack_version",
1112
+ "dispatch_id",
1113
+ "organization_id",
1114
+ "task_id",
1115
+ "scope",
1116
+ "member_agent_id",
1117
+ "agent_id",
1118
+ "session_id",
1119
+ "position_id",
1120
+ "responsibility_id",
1121
+ "reply_to",
1122
+ "route_type",
1123
+ "ack_type",
1124
+ "reply_mode",
1125
+ "plan_intent",
1126
+ "router_reason",
1127
+ "golden_route_id",
1128
+ "task_ack",
1129
+ "read_ack",
1130
+ "status",
1131
+ "note"
1132
+ ]) {
1133
+ const value = normalizeOptionalText(record[key]);
1134
+ if (value) {
1135
+ fields[key] = value;
1288
1136
  }
1289
- };
1137
+ }
1138
+ return fields;
1139
+ } catch {
1140
+ return null;
1290
1141
  }
1291
- if (!metadata) {
1292
- return {
1293
- nextMetadata: null,
1294
- dispatchAck: {
1295
- outcome: "invalid",
1296
- reason: "missing_metadata",
1297
- diagnostic: "Happy Org dispatch ack ignored: session metadata is unavailable."
1298
- }
1299
- };
1142
+ }
1143
+ function parseHappyOrgEnvelopeFields(text) {
1144
+ return parseJsonEnvelopeFields(text) ?? parseStructuredFields(text);
1145
+ }
1146
+ function getFirstDefined(record, aliases) {
1147
+ for (const alias of aliases) {
1148
+ if (Object.prototype.hasOwnProperty.call(record, alias)) {
1149
+ return record[alias];
1150
+ }
1300
1151
  }
1301
- const envelope = candidate.envelope;
1302
- const currentHappyOrg = normalizeHappyOrgMetadata(metadata);
1303
- const replyContext = cloneHappyOrgReplyContext(opts.queuedTurn?.replyContext) ?? cloneHappyOrgReplyContext(currentHappyOrg.replyContext);
1304
- if (!replyContext) {
1305
- return {
1306
- nextMetadata: metadata,
1307
- dispatchAck: {
1308
- outcome: "invalid",
1309
- reason: "missing_reply_context",
1310
- diagnostic: `Happy Org dispatch ack ignored: no active reply context matches dispatch ${envelope.dispatchId}.`
1311
- }
1312
- };
1152
+ return void 0;
1153
+ }
1154
+ function normalizeDispatchAckStatus(value) {
1155
+ const normalized = normalizeSingleLineText(value)?.toLowerCase();
1156
+ if (normalized === "accepted" || normalized === "standby" || normalized === "blocked") {
1157
+ return normalized;
1313
1158
  }
1314
- const taskContext = opts.queuedTurn?.context ?? currentHappyOrg.taskContext ?? null;
1315
- if (!taskContext) {
1316
- return {
1317
- nextMetadata: metadata,
1318
- dispatchAck: {
1319
- outcome: "invalid",
1320
- reason: "missing_task_context",
1321
- diagnostic: `Happy Org dispatch ack ignored: no active task context matches dispatch ${envelope.dispatchId}.`
1322
- }
1323
- };
1324
- }
1325
- if (envelope.dispatchId !== replyContext.dispatchId) {
1326
- return {
1327
- nextMetadata: metadata,
1328
- dispatchAck: {
1329
- outcome: "invalid",
1330
- reason: "dispatch_id_mismatch",
1331
- diagnostic: `Happy Org dispatch ack ignored: dispatch_id ${envelope.dispatchId} does not match active dispatch ${replyContext.dispatchId}.`
1332
- }
1333
- };
1334
- }
1335
- if (envelope.scope !== replyContext.scope) {
1336
- return {
1337
- nextMetadata: metadata,
1338
- dispatchAck: {
1339
- outcome: "invalid",
1340
- reason: "scope_mismatch",
1341
- diagnostic: `Happy Org dispatch ack ignored: scope ${envelope.scope} does not match active scope ${replyContext.scope}.`
1342
- }
1343
- };
1344
- }
1345
- if (envelope.taskAck !== taskContext.taskId) {
1346
- return {
1347
- nextMetadata: metadata,
1348
- dispatchAck: {
1349
- outcome: "invalid",
1350
- reason: "task_ack_mismatch",
1351
- diagnostic: `Happy Org dispatch ack ignored: task_ack ${envelope.taskAck} does not match active task ${taskContext.taskId}.`
1352
- }
1353
- };
1354
- }
1355
- if (envelope.organizationId && envelope.organizationId !== taskContext.organizationId) {
1356
- return {
1357
- nextMetadata: metadata,
1358
- dispatchAck: {
1359
- outcome: "invalid",
1360
- reason: "organization_id_mismatch",
1361
- diagnostic: `Happy Org dispatch ack ignored: organization_id ${envelope.organizationId} does not match active organization ${taskContext.organizationId}.`
1362
- }
1363
- };
1364
- }
1365
- if (envelope.taskId && envelope.taskId !== taskContext.taskId) {
1366
- return {
1367
- nextMetadata: metadata,
1368
- dispatchAck: {
1369
- outcome: "invalid",
1370
- reason: "task_id_mismatch",
1371
- diagnostic: `Happy Org dispatch ack ignored: task_id ${envelope.taskId} does not match active task ${taskContext.taskId}.`
1372
- }
1373
- };
1374
- }
1375
- if (envelope.memberAgentId && envelope.memberAgentId !== taskContext.memberAgentId) {
1376
- return {
1377
- nextMetadata: metadata,
1378
- dispatchAck: {
1379
- outcome: "invalid",
1380
- reason: "member_agent_id_mismatch",
1381
- diagnostic: `Happy Org dispatch ack ignored: member_agent_id ${envelope.memberAgentId} does not match active member ${taskContext.memberAgentId}.`
1382
- }
1383
- };
1384
- }
1385
- if (replyContext.positionId && envelope.positionId && envelope.positionId !== replyContext.positionId) {
1386
- return {
1387
- nextMetadata: metadata,
1388
- dispatchAck: {
1389
- outcome: "invalid",
1390
- reason: "position_id_mismatch",
1391
- diagnostic: `Happy Org dispatch ack ignored: position_id ${envelope.positionId} does not match active position ${replyContext.positionId}.`
1392
- }
1393
- };
1394
- }
1395
- if (replyContext.responsibilityId && envelope.responsibilityId && envelope.responsibilityId !== replyContext.responsibilityId) {
1396
- return {
1397
- nextMetadata: metadata,
1398
- dispatchAck: {
1399
- outcome: "invalid",
1400
- reason: "responsibility_id_mismatch",
1401
- diagnostic: `Happy Org dispatch ack ignored: responsibility_id ${envelope.responsibilityId} does not match active responsibility ${replyContext.responsibilityId}.`
1402
- }
1403
- };
1404
- }
1405
- if (replyContext.routeType && envelope.routeType && envelope.routeType !== replyContext.routeType) {
1406
- return {
1407
- nextMetadata: metadata,
1408
- dispatchAck: {
1409
- outcome: "invalid",
1410
- reason: "route_type_mismatch",
1411
- diagnostic: `Happy Org dispatch ack ignored: route_type ${envelope.routeType} does not match active route ${replyContext.routeType}.`
1412
- }
1413
- };
1159
+ return null;
1160
+ }
1161
+ function normalizeDispatchReadAck(value) {
1162
+ if (value === true) {
1163
+ return HAPPY_ORG_READ_ACK;
1414
1164
  }
1415
- if (replyContext.ackType && envelope.ackType && envelope.ackType !== replyContext.ackType) {
1416
- return {
1417
- nextMetadata: metadata,
1418
- dispatchAck: {
1419
- outcome: "invalid",
1420
- reason: "ack_type_mismatch",
1421
- diagnostic: `Happy Org dispatch ack ignored: ack_type ${envelope.ackType} does not match active ack_type ${replyContext.ackType}.`
1422
- }
1423
- };
1165
+ const normalized = normalizeSingleLineText(value)?.toLowerCase();
1166
+ if (!normalized) {
1167
+ return null;
1424
1168
  }
1425
- if (replyContext.replyMode && envelope.replyMode && envelope.replyMode !== replyContext.replyMode) {
1426
- return {
1427
- nextMetadata: metadata,
1428
- dispatchAck: {
1429
- outcome: "invalid",
1430
- reason: "reply_mode_mismatch",
1431
- diagnostic: `Happy Org dispatch ack ignored: reply_mode ${envelope.replyMode} does not match active reply_mode ${replyContext.replyMode}.`
1432
- }
1433
- };
1169
+ return normalized === "yes" || normalized === "true" ? HAPPY_ORG_READ_ACK : null;
1170
+ }
1171
+ function normalizeRouteType(value) {
1172
+ const normalized = normalizeSingleLineText(value)?.toLowerCase();
1173
+ if (normalized === "direct_reply" || normalized === "version_planning_reply" || normalized === "analysis_task" || normalized === "implementation_task" || normalized === "ack_plus_reply") {
1174
+ return normalized;
1434
1175
  }
1435
- const existingAck = currentHappyOrg.dispatchAcks?.[envelope.dispatchId];
1436
- if (existingAck) {
1437
- return {
1438
- nextMetadata: metadata,
1439
- dispatchAck: {
1440
- outcome: "duplicate",
1441
- reason: "already_submitted",
1442
- diagnostic: `Happy Org dispatch ack skipped: dispatch ${envelope.dispatchId} was already acknowledged as ${existingAck.status}.`
1443
- }
1444
- };
1176
+ return null;
1177
+ }
1178
+ function normalizeAckType(value) {
1179
+ const normalized = normalizeSingleLineText(value)?.toLowerCase();
1180
+ if (normalized === "none" || normalized === "read_ack" || normalized === "dispatch_ack" || normalized === "route_ack" || normalized === "progress_ack") {
1181
+ return normalized;
1445
1182
  }
1446
- if (!opts.submitDispatchAck) {
1447
- return {
1448
- nextMetadata: metadata,
1449
- dispatchAck: {
1450
- outcome: "error",
1451
- reason: "submit_callback_unavailable",
1452
- diagnostic: `Happy Org dispatch ack could not be submitted for ${envelope.dispatchId}: no submit callback is available.`
1453
- }
1454
- };
1183
+ return null;
1184
+ }
1185
+ function normalizeReplyMode(value) {
1186
+ const normalized = normalizeSingleLineText(value)?.toLowerCase();
1187
+ if (normalized === "reply-first" || normalized === "analysis-first") {
1188
+ return normalized;
1455
1189
  }
1456
- try {
1457
- await opts.submitDispatchAck({
1458
- organizationId: taskContext.organizationId,
1459
- dispatchId: envelope.dispatchId,
1460
- memberAgentId: taskContext.memberAgentId,
1461
- scope: envelope.scope,
1462
- readAck: envelope.readAck,
1463
- taskAck: envelope.taskAck,
1464
- status: envelope.status,
1465
- note: envelope.note
1466
- });
1467
- return {
1468
- nextMetadata: recordHappyOrgDispatchAck(
1469
- metadata,
1470
- envelope,
1471
- opts.now?.() ?? Date.now()
1472
- ),
1473
- dispatchAck: {
1474
- outcome: "submitted",
1475
- reason: "submitted",
1476
- diagnostic: null
1477
- }
1478
- };
1479
- } catch (error) {
1480
- const diagnostic = `Happy Org dispatch ack failed for ${envelope.dispatchId}: ${normalizeDispatchAckError(error)}.`;
1481
- logger.debug("[HappyOrg] Dispatch ack submission failed", {
1482
- dispatchId: envelope.dispatchId,
1483
- error
1484
- });
1485
- return {
1486
- nextMetadata: metadata,
1487
- dispatchAck: {
1488
- outcome: "error",
1489
- reason: "submit_failed",
1490
- diagnostic
1491
- }
1492
- };
1190
+ return null;
1191
+ }
1192
+ function normalizeAcceptanceState(value) {
1193
+ const normalized = normalizeSingleLineText(value)?.toLowerCase();
1194
+ if (normalized === "not_ready" || normalized === "awaiting_acceptance" || normalized === "closed") {
1195
+ return normalized;
1493
1196
  }
1197
+ return null;
1494
1198
  }
1495
- function deriveHomeSlug(path) {
1496
- if (!path) {
1199
+ function normalizeBlockerSummary(value) {
1200
+ const normalized = normalizeSingleLineText(value);
1201
+ if (!normalized) {
1497
1202
  return null;
1498
1203
  }
1499
- const trimmedPath = path.trim().replace(/[\\/]+$/, "");
1500
- if (!trimmedPath) {
1204
+ const lowered = normalized.toLowerCase();
1205
+ if (lowered === "none" || lowered === "null" || lowered === "no blocker") {
1501
1206
  return null;
1502
1207
  }
1503
- return basename(trimmedPath) || trimmedPath;
1208
+ return normalized;
1504
1209
  }
1505
- function buildSpecialistHomeIdentity(opts) {
1506
- const metadata = opts.metadata ?? null;
1507
- const fallback = opts.fallback ?? null;
1508
- const path = normalizeOptionalText(metadata?.path) ?? fallback?.path ?? null;
1509
- const homeSlug = deriveHomeSlug(path) ?? fallback?.homeSlug ?? null;
1510
- if (!homeSlug) {
1511
- return fallback ? { ...fallback } : null;
1210
+ function normalizeHappyOrgAcceptanceHandoffRecord(record) {
1211
+ const dispatchId = normalizeSingleLineText(getFirstDefined(record, DISPATCH_ID_ALIASES));
1212
+ const replyTo = normalizeSingleLineText(getFirstDefined(record, REPLY_TO_ALIASES));
1213
+ const taskId = normalizeSingleLineText(getFirstDefined(record, TASK_ID_ALIASES));
1214
+ const positionStatus = normalizeSingleLineText(getFirstDefined(record, POSITION_STATUS_ALIASES));
1215
+ const latestUserVisibleResult = normalizeSingleLineText(getFirstDefined(record, LATEST_USER_VISIBLE_RESULT_ALIASES));
1216
+ const acceptanceState = normalizeAcceptanceState(getFirstDefined(record, ACCEPTANCE_STATE_ALIASES));
1217
+ const ceoWriteNextStep = normalizeSingleLineText(getFirstDefined(record, CEO_WRITE_NEXT_STEP_ALIASES));
1218
+ if (!dispatchId || !replyTo || !taskId || !positionStatus || !latestUserVisibleResult || !acceptanceState || !ceoWriteNextStep) {
1219
+ return null;
1512
1220
  }
1513
1221
  return {
1514
- homeSlug,
1515
- path,
1516
- happySessionId: normalizeOptionalText(opts.sessionId) ?? fallback?.happySessionId ?? null,
1517
- machineId: normalizeOptionalText(metadata?.machineId) ?? fallback?.machineId ?? null,
1518
- startedBy: metadata?.startedBy ?? fallback?.startedBy ?? null,
1519
- flavor: normalizeOptionalText(metadata?.flavor) ?? fallback?.flavor ?? null
1520
- };
1521
- }
1522
- function withDefaultReplyRouting(replyContext) {
1523
- return {
1524
- ...replyContext,
1525
- routeType: replyContext.routeType ?? "ack_plus_reply",
1526
- ackType: replyContext.ackType ?? "dispatch_ack",
1527
- replyMode: replyContext.replyMode ?? "reply-first"
1222
+ dispatchId,
1223
+ replyTo,
1224
+ taskId,
1225
+ positionId: normalizeSingleLineText(getFirstDefined(record, POSITION_ID_ALIASES)),
1226
+ responsibilityId: normalizeSingleLineText(getFirstDefined(record, RESPONSIBILITY_ID_ALIASES)),
1227
+ positionStatus,
1228
+ latestUserVisibleResult,
1229
+ blockerSummary: normalizeBlockerSummary(getFirstDefined(record, BLOCKER_SUMMARY_ALIASES)),
1230
+ acceptanceState,
1231
+ ceoWriteNextStep
1528
1232
  };
1529
1233
  }
1530
- function resolveReplyContextFromMessage(message, taskContext) {
1531
- const metaReplyContext = message.meta?.happyOrg?.replyContext;
1532
- if (metaReplyContext?.dispatchId && metaReplyContext.scope && metaReplyContext.replyTo) {
1533
- return withDefaultReplyRouting({
1534
- dispatchId: metaReplyContext.dispatchId,
1535
- taskId: metaReplyContext.taskId ?? taskContext?.taskId ?? null,
1536
- organizationId: metaReplyContext.organizationId ?? taskContext?.organizationId ?? null,
1537
- scope: metaReplyContext.scope,
1538
- replyTo: metaReplyContext.replyTo,
1539
- memberAgentId: metaReplyContext.memberAgentId ?? taskContext?.memberAgentId ?? null,
1540
- sessionId: metaReplyContext.sessionId ?? null,
1541
- positionId: metaReplyContext.positionId ?? taskContext?.positionId ?? null,
1542
- responsibilityId: metaReplyContext.responsibilityId ?? taskContext?.responsibilityId ?? null,
1543
- routeType: metaReplyContext.routeType ?? null,
1544
- ackType: metaReplyContext.ackType ?? null,
1545
- replyMode: metaReplyContext.replyMode ?? null,
1546
- planIntent: metaReplyContext.planIntent ?? null,
1547
- routerReason: metaReplyContext.routerReason ?? null,
1548
- goldenRouteId: metaReplyContext.goldenRouteId ?? null
1549
- });
1550
- }
1551
- const fields = parseHappyOrgEnvelopeFields(message.content.text);
1552
- const dispatchId = normalizeOptionalText(fields.dispatch_id);
1553
- const scope = normalizeOptionalText(fields.scope);
1554
- const replyTo = normalizeOptionalText(fields.reply_to);
1555
- const taskId = normalizeOptionalText(fields.task_id);
1556
- if (!dispatchId || !scope || !replyTo) {
1557
- return null;
1558
- }
1559
- if (taskId && taskContext?.taskId && taskId !== taskContext.taskId) {
1234
+ function normalizeHappyOrgDispatchAckRecord(record) {
1235
+ const ackVersion = normalizeSingleLineText(getFirstDefined(record, ACK_VERSION_ALIASES));
1236
+ const dispatchId = normalizeSingleLineText(getFirstDefined(record, DISPATCH_ID_ALIASES));
1237
+ const organizationId = normalizeSingleLineText(getFirstDefined(record, ORGANIZATION_ID_ALIASES));
1238
+ const taskId = normalizeSingleLineText(getFirstDefined(record, TASK_ID_ALIASES));
1239
+ const scope = normalizeSingleLineText(getFirstDefined(record, SCOPE_ALIASES));
1240
+ const memberAgentId = normalizeSingleLineText(getFirstDefined(record, MEMBER_AGENT_ID_ALIASES));
1241
+ const sessionId = normalizeSingleLineText(getFirstDefined(record, SESSION_ID_ALIASES));
1242
+ const positionId = normalizeSingleLineText(getFirstDefined(record, POSITION_ID_ALIASES));
1243
+ const responsibilityId = normalizeSingleLineText(getFirstDefined(record, RESPONSIBILITY_ID_ALIASES));
1244
+ const routeType = normalizeRouteType(getFirstDefined(record, ROUTE_TYPE_ALIASES));
1245
+ const ackType = normalizeAckType(getFirstDefined(record, ACK_TYPE_ALIASES));
1246
+ const replyMode = normalizeReplyMode(getFirstDefined(record, REPLY_MODE_ALIASES));
1247
+ const planIntent = normalizeSingleLineText(getFirstDefined(record, PLAN_INTENT_ALIASES));
1248
+ const routerReason = normalizeSingleLineText(getFirstDefined(record, ROUTER_REASON_ALIASES));
1249
+ const goldenRouteId = normalizeSingleLineText(getFirstDefined(record, GOLDEN_ROUTE_ID_ALIASES));
1250
+ const taskAck = normalizeSingleLineText(getFirstDefined(record, TASK_ACK_ALIASES));
1251
+ const readAck = normalizeDispatchReadAck(getFirstDefined(record, READ_ACK_ALIASES));
1252
+ const status = normalizeDispatchAckStatus(getFirstDefined(record, STATUS_ALIASES));
1253
+ const note = normalizeSingleLineText(getFirstDefined(record, NOTE_ALIASES));
1254
+ if (!dispatchId || !scope || !taskAck || !readAck || !status || !note) {
1560
1255
  return null;
1561
1256
  }
1562
- return withDefaultReplyRouting({
1257
+ return {
1258
+ ackVersion,
1563
1259
  dispatchId,
1564
- taskId: taskId ?? taskContext?.taskId ?? null,
1565
- organizationId: normalizeOptionalText(fields.organization_id) ?? taskContext?.organizationId ?? null,
1260
+ organizationId,
1261
+ taskId,
1566
1262
  scope,
1567
- replyTo,
1568
- memberAgentId: normalizeOptionalText(fields.member_agent_id) ?? normalizeOptionalText(fields.agent_id) ?? taskContext?.memberAgentId ?? null,
1569
- sessionId: normalizeOptionalText(fields.session_id),
1570
- positionId: normalizeOptionalText(fields.position_id) ?? taskContext?.positionId ?? null,
1571
- responsibilityId: normalizeOptionalText(fields.responsibility_id) ?? taskContext?.responsibilityId ?? null,
1572
- routeType: normalizeRouteType(fields.route_type),
1573
- ackType: normalizeAckType(fields.ack_type),
1574
- replyMode: normalizeReplyMode(fields.reply_mode),
1575
- planIntent: normalizeOptionalText(fields.plan_intent),
1576
- routerReason: normalizeOptionalText(fields.router_reason),
1577
- goldenRouteId: normalizeOptionalText(fields.golden_route_id)
1578
- });
1579
- }
1580
- function buildTerminatedStatusMessage(taskId) {
1581
- return `Task ${taskId} is terminated. Reopen it with new context, a new decision, or a new resource before continuing.`;
1263
+ memberAgentId,
1264
+ sessionId,
1265
+ positionId,
1266
+ responsibilityId,
1267
+ routeType,
1268
+ ackType,
1269
+ replyMode,
1270
+ planIntent,
1271
+ routerReason,
1272
+ goldenRouteId,
1273
+ taskAck,
1274
+ readAck,
1275
+ status,
1276
+ note
1277
+ };
1582
1278
  }
1583
- function buildMemberBusyStatusMessage(memberAgentId, activeTaskId, nextTaskId) {
1584
- return `Member ${memberAgentId} is already busy with active task ${activeTaskId}. Reject task ${nextTaskId} without continuing token usage.`;
1279
+ function isHappyOrgDispatchAckCandidateRecord(record) {
1280
+ return [
1281
+ ...DISPATCH_ID_ALIASES,
1282
+ ...ACK_VERSION_ALIASES,
1283
+ ...ORGANIZATION_ID_ALIASES,
1284
+ ...TASK_ID_ALIASES,
1285
+ ...MEMBER_AGENT_ID_ALIASES,
1286
+ ...SESSION_ID_ALIASES,
1287
+ ...POSITION_ID_ALIASES,
1288
+ ...RESPONSIBILITY_ID_ALIASES,
1289
+ ...ROUTE_TYPE_ALIASES,
1290
+ ...ACK_TYPE_ALIASES,
1291
+ ...REPLY_MODE_ALIASES,
1292
+ ...PLAN_INTENT_ALIASES,
1293
+ ...ROUTER_REASON_ALIASES,
1294
+ ...GOLDEN_ROUTE_ID_ALIASES,
1295
+ ...TASK_ACK_ALIASES,
1296
+ ...SCOPE_ALIASES,
1297
+ ...READ_ACK_ALIASES,
1298
+ ...STATUS_ALIASES,
1299
+ ...NOTE_ALIASES
1300
+ ].some((key) => Object.prototype.hasOwnProperty.call(record, key));
1585
1301
  }
1586
- function buildOwnerConflictStatusMessage(taskId, ownerAgentId) {
1587
- return `Task ${taskId} is already active under owner ${ownerAgentId}. This turn must exit without continuing token usage.`;
1302
+ function isHappyOrgAcceptanceHandoffCandidateRecord(record) {
1303
+ return [
1304
+ ...DISPATCH_ID_ALIASES,
1305
+ ...REPLY_TO_ALIASES,
1306
+ ...TASK_ID_ALIASES,
1307
+ ...POSITION_ID_ALIASES,
1308
+ ...RESPONSIBILITY_ID_ALIASES,
1309
+ ...POSITION_STATUS_ALIASES,
1310
+ ...LATEST_USER_VISIBLE_RESULT_ALIASES,
1311
+ ...BLOCKER_SUMMARY_ALIASES,
1312
+ ...ACCEPTANCE_STATE_ALIASES,
1313
+ ...CEO_WRITE_NEXT_STEP_ALIASES
1314
+ ].some((key) => Object.prototype.hasOwnProperty.call(record, key));
1588
1315
  }
1589
- function inferDraftTurnStatus(draft) {
1590
- if (draft?.turnStatus === "turn_update" || draft?.turnStatus === "task_complete") {
1591
- return draft.turnStatus;
1316
+ function extractKeyValueBlocks(text) {
1317
+ const blocks = [];
1318
+ let currentBlock = null;
1319
+ for (const rawLine of text.split(/\r?\n/)) {
1320
+ const line = rawLine.trim();
1321
+ const match = KEY_VALUE_LINE_PATTERN.exec(line);
1322
+ if (match) {
1323
+ currentBlock ??= {};
1324
+ currentBlock[match[1]] = match[2].trim();
1325
+ continue;
1326
+ }
1327
+ if (currentBlock && Object.keys(currentBlock).length > 0) {
1328
+ blocks.push(currentBlock);
1329
+ currentBlock = null;
1330
+ }
1592
1331
  }
1593
- return null;
1594
- }
1595
- function inferInterventionType(draft) {
1596
- if (draft?.interventionType === "none" || draft?.interventionType === "review_needed" || draft?.interventionType === "blocker" || draft?.interventionType === "decision_needed") {
1597
- return draft.interventionType;
1332
+ if (currentBlock && Object.keys(currentBlock).length > 0) {
1333
+ blocks.push(currentBlock);
1598
1334
  }
1599
- if (normalizeOptionalText(draft?.decisionNeeded)) {
1600
- return "decision_needed";
1335
+ return blocks;
1336
+ }
1337
+ function resolveHappyOrgDispatchAckEnvelopeCandidate(text) {
1338
+ const trimmed = stripCodeFence(text).trim();
1339
+ if (!trimmed) {
1340
+ return { outcome: "absent" };
1601
1341
  }
1602
- if (normalizeOptionalText(draft?.blockerCode)) {
1603
- return "blocker";
1342
+ let sawCandidate = false;
1343
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
1344
+ try {
1345
+ const parsed = JSON.parse(trimmed);
1346
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1347
+ const record = parsed;
1348
+ if (isHappyOrgDispatchAckCandidateRecord(record)) {
1349
+ const envelope = normalizeHappyOrgDispatchAckRecord(record);
1350
+ if (envelope) {
1351
+ return {
1352
+ outcome: "valid",
1353
+ envelope
1354
+ };
1355
+ }
1356
+ sawCandidate = true;
1357
+ }
1358
+ }
1359
+ } catch {
1360
+ }
1604
1361
  }
1605
- return "none";
1606
- }
1607
- function buildFallbackSummary(text, turnStatus) {
1608
- const trimmed = text.trim();
1609
- if (trimmed) {
1610
- return trimmed.replace(/\s+/g, " ").slice(0, HAPPY_ORG_SUMMARY_MAX_LENGTH);
1362
+ for (const block of extractKeyValueBlocks(text)) {
1363
+ if (!isHappyOrgDispatchAckCandidateRecord(block)) {
1364
+ continue;
1365
+ }
1366
+ const envelope = normalizeHappyOrgDispatchAckRecord(block);
1367
+ if (envelope) {
1368
+ return {
1369
+ outcome: "valid",
1370
+ envelope
1371
+ };
1372
+ }
1373
+ sawCandidate = true;
1611
1374
  }
1612
- return turnStatus === "turn_aborted" ? "Turn aborted before completion." : "Turn completed without a textual summary.";
1613
- }
1614
- function normalizeTurnReportDraft(draft) {
1615
- return {
1616
- turnStatus: inferDraftTurnStatus(draft),
1617
- summary: normalizeSummaryText(draft?.summary),
1618
- interventionType: inferInterventionType(draft),
1619
- blockerCode: normalizeOptionalText(draft?.blockerCode),
1620
- decisionNeeded: normalizeOptionalText(draft?.decisionNeeded),
1621
- targetArtifact: normalizePreviewableArtifactTarget(draft?.targetArtifact),
1622
- accessChannelState: normalizeAccessChannelState(draft?.accessChannelState)
1375
+ return sawCandidate ? {
1376
+ outcome: "invalid",
1377
+ diagnostic: "Happy Org dispatch ack ignored: malformed envelope. Required fields are dispatch_id, scope, task_ack, read_ack=yes, status, and note."
1378
+ } : {
1379
+ outcome: "absent"
1623
1380
  };
1624
1381
  }
1625
- function stripCodeFence(text) {
1626
- return text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
1627
- }
1628
- function extractTaggedTurnReport(text) {
1629
- const matcher = new RegExp(
1630
- `<${HAPPY_ORG_TURN_REPORT_TAG}>\\s*([\\s\\S]*?)\\s*</${HAPPY_ORG_TURN_REPORT_TAG}>`,
1631
- "gi"
1632
- );
1633
- let lastMatch = null;
1634
- for (let match = matcher.exec(text); match; match = matcher.exec(text)) {
1635
- lastMatch = match;
1382
+ function resolveHappyOrgAcceptanceHandoffCandidate(text) {
1383
+ const trimmed = stripCodeFence(text).trim();
1384
+ if (!trimmed) {
1385
+ return { outcome: "absent" };
1636
1386
  }
1637
- if (!lastMatch) {
1638
- return {
1639
- cleanedText: text.trim(),
1640
- draft: null
1641
- };
1387
+ let sawCandidate = false;
1388
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
1389
+ try {
1390
+ const parsed = JSON.parse(trimmed);
1391
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1392
+ const record = parsed;
1393
+ if (isHappyOrgAcceptanceHandoffCandidateRecord(record)) {
1394
+ const handoff = normalizeHappyOrgAcceptanceHandoffRecord(record);
1395
+ if (handoff) {
1396
+ return {
1397
+ outcome: "valid",
1398
+ handoff
1399
+ };
1400
+ }
1401
+ sawCandidate = true;
1402
+ }
1403
+ }
1404
+ } catch {
1405
+ }
1642
1406
  }
1643
- const rawBlock = stripCodeFence(lastMatch[1] ?? "");
1644
- let draft = null;
1645
- try {
1646
- const parsed = JSON.parse(rawBlock);
1647
- draft = {
1648
- turnStatus: normalizeOptionalText(parsed.turnStatus),
1649
- summary: normalizeSummaryText(parsed.summary),
1650
- interventionType: normalizeOptionalText(parsed.interventionType),
1651
- blockerCode: normalizeOptionalText(parsed.blockerCode),
1652
- decisionNeeded: normalizeOptionalText(parsed.decisionNeeded),
1653
- targetArtifact: normalizePreviewableArtifactTarget(parsed.targetArtifact),
1654
- accessChannelState: normalizeAccessChannelState(parsed.accessChannelState)
1655
- };
1656
- } catch {
1657
- draft = null;
1407
+ for (const block of extractKeyValueBlocks(text)) {
1408
+ if (!isHappyOrgAcceptanceHandoffCandidateRecord(block)) {
1409
+ continue;
1410
+ }
1411
+ const handoff = normalizeHappyOrgAcceptanceHandoffRecord(block);
1412
+ if (handoff) {
1413
+ return {
1414
+ outcome: "valid",
1415
+ handoff
1416
+ };
1417
+ }
1418
+ sawCandidate = true;
1658
1419
  }
1659
- const cleanedText = `${text.slice(0, lastMatch.index)}${text.slice(lastMatch.index + lastMatch[0].length)}`.replace(/\n{3,}/g, "\n\n").trim();
1660
- return {
1661
- cleanedText,
1662
- draft
1420
+ return sawCandidate ? {
1421
+ outcome: "invalid",
1422
+ diagnostic: "Happy Org acceptance handoff ignored: malformed mini-pack. Required fields are dispatch_id, reply_to, task_id, position_status, latest_user_visible_result, acceptance_state, and ceo_write_next_step."
1423
+ } : {
1424
+ outcome: "absent"
1663
1425
  };
1664
1426
  }
1665
- function buildRepeatFingerprint(context, blockerCode, targetArtifact) {
1666
- if (!blockerCode) {
1667
- return null;
1668
- }
1669
- return [
1670
- context.taskId,
1671
- context.memberAgentId,
1672
- blockerCode,
1673
- targetArtifact ?? ""
1674
- ].join("::");
1427
+ function recordHappyOrgDispatchAck(metadata, ack, acknowledgedAt) {
1428
+ const nextHappyOrg = normalizeHappyOrgMetadata(metadata);
1429
+ nextHappyOrg.dispatchAcks = nextHappyOrg.dispatchAcks ?? {};
1430
+ nextHappyOrg.dispatchAcks[ack.dispatchId] = {
1431
+ dispatchId: ack.dispatchId,
1432
+ ackVersion: ack.ackVersion,
1433
+ scope: ack.scope,
1434
+ taskAck: ack.taskAck,
1435
+ readAck: ack.readAck,
1436
+ status: ack.status,
1437
+ note: ack.note,
1438
+ taskId: ack.taskId,
1439
+ organizationId: ack.organizationId,
1440
+ memberAgentId: ack.memberAgentId,
1441
+ sessionId: ack.sessionId,
1442
+ positionId: ack.positionId,
1443
+ responsibilityId: ack.responsibilityId,
1444
+ routeType: ack.routeType,
1445
+ ackType: ack.ackType,
1446
+ replyMode: ack.replyMode,
1447
+ planIntent: ack.planIntent,
1448
+ routerReason: ack.routerReason,
1449
+ goldenRouteId: ack.goldenRouteId,
1450
+ acknowledgedAt
1451
+ };
1452
+ return withHappyOrgMetadata(metadata, nextHappyOrg);
1675
1453
  }
1676
- function resolveReportedTurnStatus(transportTurnStatus, draft) {
1677
- if (transportTurnStatus === "turn_aborted") {
1678
- return "turn_aborted";
1454
+ function normalizeDispatchAckError(error) {
1455
+ if (error instanceof Error) {
1456
+ return error.message.trim() || "Unknown error";
1679
1457
  }
1680
- return draft.turnStatus === "task_complete" ? "task_complete" : "turn_update";
1458
+ if (typeof error === "string") {
1459
+ return error.trim() || "Unknown error";
1460
+ }
1461
+ return "Unknown error";
1681
1462
  }
1682
- function buildRuntimeStateAfterTurn(report) {
1683
- if (report.turnStatus === "task_complete") {
1463
+ async function maybeSubmitHappyOrgDispatchBusinessAck(opts) {
1464
+ const metadata = opts.metadata ?? null;
1465
+ const candidate = resolveHappyOrgDispatchAckEnvelopeCandidate(opts.responseText);
1466
+ if (candidate.outcome === "absent") {
1684
1467
  return {
1685
- status: "waiting_close",
1686
- reason: "awaiting_ceo_close"
1468
+ nextMetadata: metadata,
1469
+ dispatchAck: {
1470
+ outcome: "none",
1471
+ reason: "no_envelope",
1472
+ diagnostic: null
1473
+ }
1687
1474
  };
1688
1475
  }
1689
- switch (report.interventionType) {
1690
- case "decision_needed":
1691
- return {
1692
- status: "waiting_decision",
1693
- reason: "awaiting_user_decision"
1694
- };
1695
- case "review_needed":
1696
- return {
1697
- status: "waiting_review",
1698
- reason: "awaiting_ceo_review"
1699
- };
1700
- case "blocker":
1701
- return {
1702
- status: "waiting_review",
1703
- reason: "awaiting_ceo_context"
1704
- };
1705
- default:
1706
- return {
1707
- status: "active",
1708
- reason: null
1709
- };
1476
+ if (candidate.outcome === "invalid") {
1477
+ logger.debug("[HappyOrg] Dispatch ack candidate rejected as malformed");
1478
+ return {
1479
+ nextMetadata: metadata,
1480
+ dispatchAck: {
1481
+ outcome: "invalid",
1482
+ reason: "malformed_envelope",
1483
+ diagnostic: candidate.diagnostic
1484
+ }
1485
+ };
1710
1486
  }
1711
- }
1712
- function buildHappyOrgTurnPrompt(prompt, turn) {
1713
- const reopenLines = turn.reopenContext ? [
1714
- "",
1715
- "This task was explicitly reopened for this turn with the following new inputs:",
1716
- turn.reopenContext.newContext ? `- newContext: ${turn.reopenContext.newContext}` : null,
1717
- turn.reopenContext.newDecision ? `- newDecision: ${turn.reopenContext.newDecision}` : null,
1718
- turn.reopenContext.newResource ? `- newResource: ${turn.reopenContext.newResource}` : null
1719
- ].filter(Boolean) : [];
1720
- const replyContextLines = turn.replyContext ? [
1721
- "",
1722
- "This turn also carries a formal dispatch reply context. If you acknowledge or update that dispatch, keep these values stable:",
1723
- `dispatch_id=${turn.replyContext.dispatchId}`,
1724
- turn.replyContext.organizationId ? `organization_id=${turn.replyContext.organizationId}` : null,
1725
- turn.replyContext.taskId ? `task_id=${turn.replyContext.taskId}` : null,
1726
- `scope=${turn.replyContext.scope}`,
1727
- `reply_to=${turn.replyContext.replyTo}`,
1728
- turn.replyContext.memberAgentId ? `member_agent_id=${turn.replyContext.memberAgentId}` : null,
1729
- turn.replyContext.sessionId ? `session_id=${turn.replyContext.sessionId}` : null,
1730
- turn.replyContext.positionId ? `position_id=${turn.replyContext.positionId}` : null,
1731
- turn.replyContext.responsibilityId ? `responsibility_id=${turn.replyContext.responsibilityId}` : null,
1732
- turn.replyContext.routeType ? `route_type=${turn.replyContext.routeType}` : null,
1733
- turn.replyContext.ackType ? `ack_type=${turn.replyContext.ackType}` : null,
1734
- turn.replyContext.replyMode ? `reply_mode=${turn.replyContext.replyMode}` : null,
1735
- "If the route feels ambiguous or a routing skill misses, stay reply-first / ack_plus_reply and do not start repo or code analysis by default.",
1736
- "When you send the formal dispatch business ack in your visible response, include exactly one raw key=value block (no markdown code fence) using this shape:",
1737
- `ack_version=${HAPPY_ORG_REPLY_ACK_VERSION}`,
1738
- `dispatch_id=${turn.replyContext.dispatchId}`,
1739
- turn.replyContext.organizationId ? `organization_id=${turn.replyContext.organizationId}` : null,
1740
- turn.replyContext.taskId ? `task_id=${turn.replyContext.taskId}` : null,
1741
- `scope=${turn.replyContext.scope}`,
1742
- `member_agent_id=${turn.replyContext.memberAgentId ?? turn.context.memberAgentId}`,
1743
- turn.replyContext.sessionId ? `session_id=${turn.replyContext.sessionId}` : null,
1744
- turn.replyContext.positionId ? `position_id=${turn.replyContext.positionId}` : null,
1745
- turn.replyContext.responsibilityId ? `responsibility_id=${turn.replyContext.responsibilityId}` : null,
1746
- `route_type=${turn.replyContext.routeType ?? "ack_plus_reply"}`,
1747
- `ack_type=${turn.replyContext.ackType ?? "dispatch_ack"}`,
1748
- `reply_mode=${turn.replyContext.replyMode ?? "reply-first"}`,
1749
- `task_ack=${turn.context.taskId}`,
1750
- "read_ack=yes",
1751
- "status=accepted | standby | blocked",
1752
- "note=<short note>",
1753
- "Keep dispatch_id / task_id / position_id / responsibility_id stable across the visible ack, visible reply, and final turn report.",
1754
- "When turnStatus=task_complete and reply context exists, include a short source reply for the original requester at reply_to before the final JSON block.",
1755
- "That source reply / acceptance handoff mini-pack should explicitly mention position_status, latest_user_visible_result, blocker_summary, and acceptance_state=awaiting_acceptance.",
1756
- "Use acceptance_state=closed only after CEO or the original requester actually confirms close; do not self-mark closed."
1757
- ].filter(Boolean) : [];
1758
- const specialistHomeLines = turn.specialistHome ? [
1759
- "",
1760
- "Reply from this specialist home identity when applicable:",
1761
- `home_slug=${turn.specialistHome.homeSlug}`,
1762
- turn.specialistHome.happySessionId ? `happy_session_id=${turn.specialistHome.happySessionId}` : null,
1763
- turn.specialistHome.machineId ? `machine_id=${turn.specialistHome.machineId}` : null
1764
- ].filter(Boolean) : [];
1765
- const header = [
1766
- "[HAPPY_ORG_TASK_CONTEXT]",
1767
- `taskId=${turn.context.taskId}`,
1768
- `organizationId=${turn.context.organizationId}`,
1769
- turn.context.organizationRootPath ? `organizationRootPath=${turn.context.organizationRootPath}` : null,
1770
- `memberAgentId=${turn.context.memberAgentId}`,
1771
- `supervisorAgentId=${turn.context.supervisorAgentId}`,
1772
- turn.context.positionId ? `positionId=${turn.context.positionId}` : null,
1773
- turn.context.responsibilityId ? `responsibilityId=${turn.context.responsibilityId}` : null,
1774
- "Stay on this exact task for the whole turn.",
1775
- "End your response with exactly one raw JSON block inside these tags and do not wrap it in a markdown code fence:",
1776
- `<${HAPPY_ORG_TURN_REPORT_TAG}>{"turnStatus":"turn_update","summary":"short task-board summary","interventionType":"none","blockerCode":null,"decisionNeeded":null,"targetArtifact":null,"accessChannelState":"ok"}</${HAPPY_ORG_TURN_REPORT_TAG}>`,
1777
- "Allowed turnStatus values in the JSON block: turn_update, task_complete.",
1778
- "Allowed interventionType values: none, review_needed, blocker, decision_needed.",
1779
- "Allowed accessChannelState values: ok, reattach_required, runtime_replaced.",
1780
- "Use turnStatus=task_complete only when you believe the assigned task is finished and ready for CEO acceptance.",
1781
- "If turnStatus=task_complete, the task will wait for CEO close; do not treat it as automatically closed.",
1782
- "If turnStatus=task_complete, interventionType must be review_needed.",
1783
- "targetArtifact must be null or a concrete previewable file target. Use a repo-relative path, absolute path, file:// URL, or https:// URL when you actually produced a material or artifact the client should open directly.",
1784
- "Do not use abstract labels like release-checklist-template or completion-board-template in targetArtifact.",
1785
- `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.`,
1786
- "Use blocker only for problems the organization may still solve internally.",
1787
- "Use decision_needed only when a CEO or user decision is required.",
1788
- "Use review_needed when supervisor review is needed but the work is not blocked.",
1789
- ...reopenLines,
1790
- ...replyContextLines,
1791
- ...specialistHomeLines,
1792
- "[/HAPPY_ORG_TASK_CONTEXT]",
1793
- "",
1794
- prompt
1795
- ].filter(Boolean);
1796
- return header.join("\n");
1797
- }
1798
- function resolveHappyOrgQueuedTurn(opts) {
1799
- const metadata = opts.metadata ?? null;
1800
1487
  if (!metadata) {
1801
1488
  return {
1802
1489
  nextMetadata: null,
1803
- queuedTurn: null,
1804
- blocked: false
1490
+ dispatchAck: {
1491
+ outcome: "invalid",
1492
+ reason: "missing_metadata",
1493
+ diagnostic: "Happy Org dispatch ack ignored: session metadata is unavailable."
1494
+ }
1805
1495
  };
1806
1496
  }
1497
+ const envelope = candidate.envelope;
1807
1498
  const currentHappyOrg = normalizeHappyOrgMetadata(metadata);
1808
- let nextHappyOrg = cloneHappyOrgMetadata(currentHappyOrg);
1809
- const messageHappyOrg = opts.message.meta?.happyOrg;
1810
- const now = opts.now?.() ?? Date.now();
1811
- const createRunId = opts.createRunId ?? ((taskContext2, currentNow) => `${taskContext2.taskId}:${taskContext2.memberAgentId}:${currentNow}`);
1812
- const specialistHome = buildSpecialistHomeIdentity({
1813
- metadata,
1814
- sessionId: opts.sessionId,
1815
- fallback: currentHappyOrg.specialistHome
1816
- });
1817
- nextHappyOrg.specialistHome = cloneHappyOrgSpecialistHomeIdentity(specialistHome);
1818
- if (messageHappyOrg?.taskContext) {
1819
- const currentTaskId = currentHappyOrg.taskContext?.taskId ?? null;
1820
- if (currentTaskId !== messageHappyOrg.taskContext.taskId) {
1821
- if (currentHappyOrg.taskContext && currentHappyOrg.runtime?.status === "active") {
1822
- return {
1823
- nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
1824
- queuedTurn: null,
1825
- blocked: true,
1826
- statusMessage: buildMemberBusyStatusMessage(
1827
- currentHappyOrg.taskContext.memberAgentId,
1828
- currentHappyOrg.taskContext.taskId,
1829
- messageHappyOrg.taskContext.taskId
1830
- )
1831
- };
1499
+ const replyContext = cloneHappyOrgReplyContext(opts.queuedTurn?.replyContext) ?? cloneHappyOrgReplyContext(currentHappyOrg.replyContext);
1500
+ if (!replyContext) {
1501
+ return {
1502
+ nextMetadata: metadata,
1503
+ dispatchAck: {
1504
+ outcome: "invalid",
1505
+ reason: "missing_reply_context",
1506
+ diagnostic: `Happy Org dispatch ack ignored: no active reply context matches dispatch ${envelope.dispatchId}.`
1832
1507
  }
1833
- nextHappyOrg = resetHappyOrgRuntimeForTask(messageHappyOrg.taskContext, specialistHome);
1834
- } else {
1835
- nextHappyOrg.taskContext = { ...messageHappyOrg.taskContext };
1836
- }
1837
- }
1838
- const taskContext = nextHappyOrg.taskContext ?? null;
1839
- const replyContext = resolveReplyContextFromMessage(opts.message, taskContext);
1840
- if (replyContext) {
1841
- nextHappyOrg.replyContext = replyContext;
1842
- } else if (!taskContext || currentHappyOrg.taskContext?.taskId !== taskContext.taskId) {
1843
- nextHappyOrg.replyContext = null;
1508
+ };
1844
1509
  }
1510
+ const taskContext = opts.queuedTurn?.context ?? currentHappyOrg.taskContext ?? null;
1845
1511
  if (!taskContext) {
1846
1512
  return {
1847
1513
  nextMetadata: metadata,
1848
- queuedTurn: null,
1849
- blocked: false
1514
+ dispatchAck: {
1515
+ outcome: "invalid",
1516
+ reason: "missing_task_context",
1517
+ diagnostic: `Happy Org dispatch ack ignored: no active task context matches dispatch ${envelope.dispatchId}.`
1518
+ }
1850
1519
  };
1851
1520
  }
1852
- const control = messageHappyOrg?.control;
1853
- if (control?.action === "terminate") {
1854
- nextHappyOrg.runtime = {
1855
- status: "terminated",
1856
- reason: normalizeOptionalText(control.reason) ?? "terminated_by_supervisor",
1857
- terminatedAt: now
1858
- };
1859
- nextHappyOrg.activeOwner = null;
1521
+ if (envelope.dispatchId !== replyContext.dispatchId) {
1860
1522
  return {
1861
- nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
1862
- queuedTurn: null,
1863
- blocked: true,
1864
- statusMessage: buildTerminatedStatusMessage(taskContext.taskId)
1523
+ nextMetadata: metadata,
1524
+ dispatchAck: {
1525
+ outcome: "invalid",
1526
+ reason: "dispatch_id_mismatch",
1527
+ diagnostic: `Happy Org dispatch ack ignored: dispatch_id ${envelope.dispatchId} does not match active dispatch ${replyContext.dispatchId}.`
1528
+ }
1865
1529
  };
1866
1530
  }
1867
- const hasReopenInputs = Boolean(
1868
- normalizeOptionalText(control?.newContext) || normalizeOptionalText(control?.newDecision) || normalizeOptionalText(control?.newResource)
1869
- );
1870
- if (nextHappyOrg.runtime?.status === "terminated") {
1871
- if (control?.action !== "reopen") {
1872
- return {
1873
- nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
1874
- queuedTurn: null,
1875
- blocked: true,
1876
- statusMessage: buildTerminatedStatusMessage(taskContext.taskId)
1877
- };
1878
- }
1879
- if (!hasReopenInputs) {
1880
- return {
1881
- nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
1882
- queuedTurn: null,
1883
- blocked: true,
1884
- statusMessage: `Task ${taskContext.taskId} can only reopen with new context, a new decision, or a new resource.`
1885
- };
1886
- }
1887
- }
1888
- const reopenContext = control?.action === "reopen" && hasReopenInputs ? {
1889
- newContext: normalizeOptionalText(control.newContext),
1890
- newDecision: normalizeOptionalText(control.newDecision),
1891
- newResource: normalizeOptionalText(control.newResource)
1892
- } : void 0;
1893
- if (reopenContext) {
1894
- nextHappyOrg.runtime = {
1895
- status: "active",
1896
- reason: null,
1897
- reopenedAt: now
1898
- };
1899
- } else if (!nextHappyOrg.runtime || nextHappyOrg.runtime.status !== "active") {
1900
- nextHappyOrg.runtime = {
1901
- status: "active",
1902
- reason: null
1531
+ if (envelope.scope !== replyContext.scope) {
1532
+ return {
1533
+ nextMetadata: metadata,
1534
+ dispatchAck: {
1535
+ outcome: "invalid",
1536
+ reason: "scope_mismatch",
1537
+ diagnostic: `Happy Org dispatch ack ignored: scope ${envelope.scope} does not match active scope ${replyContext.scope}.`
1538
+ }
1903
1539
  };
1904
1540
  }
1905
- if (!nextHappyOrg.activeOwner) {
1906
- nextHappyOrg.activeOwner = {
1907
- ownerAgentId: taskContext.memberAgentId,
1908
- ownerRunId: createRunId(taskContext, now),
1909
- claimedAt: now
1910
- };
1911
- } else if (nextHappyOrg.activeOwner.ownerAgentId !== taskContext.memberAgentId) {
1541
+ if (envelope.taskAck !== taskContext.taskId) {
1912
1542
  return {
1913
- nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
1914
- queuedTurn: null,
1915
- blocked: true,
1916
- statusMessage: buildOwnerConflictStatusMessage(
1917
- taskContext.taskId,
1918
- nextHappyOrg.activeOwner.ownerAgentId
1919
- )
1543
+ nextMetadata: metadata,
1544
+ dispatchAck: {
1545
+ outcome: "invalid",
1546
+ reason: "task_ack_mismatch",
1547
+ diagnostic: `Happy Org dispatch ack ignored: task_ack ${envelope.taskAck} does not match active task ${taskContext.taskId}.`
1548
+ }
1920
1549
  };
1921
1550
  }
1922
- return {
1923
- nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
1924
- queuedTurn: {
1925
- context: taskContext,
1926
- ...reopenContext ? { reopenContext } : {},
1927
- replyContext: cloneHappyOrgReplyContext(nextHappyOrg.replyContext),
1928
- specialistHome: cloneHappyOrgSpecialistHomeIdentity(nextHappyOrg.specialistHome)
1929
- },
1930
- blocked: false
1931
- };
1932
- }
1933
- function finalizeHappyOrgTurn(opts) {
1934
- const metadata = opts.metadata ?? null;
1935
- const queuedTurn = opts.queuedTurn ?? null;
1936
- const { cleanedText, draft } = extractTaggedTurnReport(opts.responseText);
1937
- if (!metadata || !queuedTurn) {
1551
+ if (envelope.organizationId && envelope.organizationId !== taskContext.organizationId) {
1938
1552
  return {
1939
- cleanedText,
1940
- report: null,
1941
- nextMetadata: metadata
1553
+ nextMetadata: metadata,
1554
+ dispatchAck: {
1555
+ outcome: "invalid",
1556
+ reason: "organization_id_mismatch",
1557
+ diagnostic: `Happy Org dispatch ack ignored: organization_id ${envelope.organizationId} does not match active organization ${taskContext.organizationId}.`
1558
+ }
1942
1559
  };
1943
1560
  }
1944
- const now = opts.now?.() ?? Date.now();
1945
- const normalizedDraft = normalizeTurnReportDraft(draft);
1946
- const reportTurnStatus = resolveReportedTurnStatus(opts.turnStatus, normalizedDraft);
1947
- const currentHappyOrg = normalizeHappyOrgMetadata(metadata);
1948
- const resolvedReplyContext = cloneHappyOrgReplyContext(queuedTurn.replyContext) ?? cloneHappyOrgReplyContext(currentHappyOrg.replyContext);
1949
- const resolvedSpecialistHome = cloneHappyOrgSpecialistHomeIdentity(queuedTurn.specialistHome) ?? cloneHappyOrgSpecialistHomeIdentity(currentHappyOrg.specialistHome);
1950
- const acceptanceHandoffCandidate = resolveHappyOrgAcceptanceHandoffCandidate(cleanedText);
1951
- const acceptanceHandoff = acceptanceHandoffCandidate.outcome === "valid" && acceptanceHandoffCandidate.handoff.taskId === queuedTurn.context.taskId && (!resolvedReplyContext || acceptanceHandoffCandidate.handoff.dispatchId === resolvedReplyContext.dispatchId) ? acceptanceHandoffCandidate.handoff : null;
1952
- const report = {
1953
- ...queuedTurn.context,
1954
- turnStatus: reportTurnStatus,
1955
- interventionType: reportTurnStatus === "task_complete" ? "review_needed" : normalizedDraft.interventionType ?? "none",
1956
- summary: normalizedDraft.summary ?? buildFallbackSummary(cleanedText, reportTurnStatus),
1957
- blockerCode: normalizedDraft.blockerCode ?? null,
1958
- decisionNeeded: normalizedDraft.decisionNeeded ?? null,
1959
- targetArtifact: normalizedDraft.targetArtifact ?? null,
1960
- accessChannelState: normalizedDraft.accessChannelState ?? "ok",
1961
- repeatFingerprint: buildRepeatFingerprint(
1962
- queuedTurn.context,
1963
- normalizedDraft.blockerCode ?? null,
1964
- normalizedDraft.targetArtifact ?? null
1965
- ),
1966
- replyContext: resolvedReplyContext,
1967
- specialistHome: resolvedSpecialistHome,
1968
- ...acceptanceHandoff ? { acceptanceHandoff } : {}
1969
- };
1970
- const nextHappyOrg = currentHappyOrg;
1971
- nextHappyOrg.taskContext = { ...queuedTurn.context };
1972
- nextHappyOrg.lastTurnReport = report;
1973
- nextHappyOrg.activeOwner = null;
1974
- nextHappyOrg.replyContext = resolvedReplyContext;
1975
- nextHappyOrg.specialistHome = resolvedSpecialistHome;
1976
- nextHappyOrg.repeat = nextHappyOrg.repeat ?? {
1977
- threshold: HAPPY_ORG_REPEAT_THRESHOLD,
1978
- fingerprints: {}
1979
- };
1980
- let terminateMessage;
1981
- if (report.repeatFingerprint) {
1982
- const currentEntry = nextHappyOrg.repeat.fingerprints[report.repeatFingerprint] ?? {
1983
- count: 0};
1984
- const nextCount = currentEntry.count + 1;
1985
- nextHappyOrg.repeat.fingerprints[report.repeatFingerprint] = {
1986
- count: nextCount,
1987
- lastSeenAt: now
1561
+ if (envelope.taskId && envelope.taskId !== taskContext.taskId) {
1562
+ return {
1563
+ nextMetadata: metadata,
1564
+ dispatchAck: {
1565
+ outcome: "invalid",
1566
+ reason: "task_id_mismatch",
1567
+ diagnostic: `Happy Org dispatch ack ignored: task_id ${envelope.taskId} does not match active task ${taskContext.taskId}.`
1568
+ }
1988
1569
  };
1989
- if (nextCount >= nextHappyOrg.repeat.threshold) {
1990
- nextHappyOrg.runtime = {
1991
- status: "terminated",
1992
- reason: `repeat_fingerprint:${report.repeatFingerprint}`,
1993
- terminatedAt: now
1994
- };
1995
- terminateMessage = `Task ${queuedTurn.context.taskId} hit repeat threshold for ${report.repeatFingerprint} and is now terminated until reopen.`;
1996
- } else if (!nextHappyOrg.runtime || nextHappyOrg.runtime.status !== "terminated") {
1997
- nextHappyOrg.runtime = buildRuntimeStateAfterTurn(report);
1998
- }
1999
- } else if (!nextHappyOrg.runtime || nextHappyOrg.runtime.status !== "terminated") {
2000
- nextHappyOrg.runtime = buildRuntimeStateAfterTurn(report);
2001
1570
  }
2002
- return {
2003
- cleanedText,
2004
- report,
2005
- nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
2006
- terminateMessage
2007
- };
2008
- }
2009
- async function finalizeHappyOrgTurnWithBusinessAck(opts) {
2010
- const finalizedTurn = finalizeHappyOrgTurn({
2011
- metadata: opts.metadata,
2012
- queuedTurn: opts.queuedTurn,
2013
- responseText: opts.responseText,
2014
- turnStatus: opts.turnStatus,
2015
- now: opts.now
2016
- });
2017
- const dispatchAckResult = await maybeSubmitHappyOrgDispatchBusinessAck({
2018
- metadata: finalizedTurn.nextMetadata,
2019
- queuedTurn: opts.queuedTurn,
2020
- responseText: finalizedTurn.cleanedText,
2021
- now: opts.now,
2022
- submitDispatchAck: opts.submitDispatchAck
2023
- });
2024
- return {
2025
- ...finalizedTurn,
2026
- nextMetadata: dispatchAckResult.nextMetadata,
2027
- dispatchAck: dispatchAckResult.dispatchAck
2028
- };
2029
- }
2030
-
2031
- const DEFAULT_MAX_MESSAGES = 200;
2032
- const DEFAULT_MAX_CHARACTERS = 1e5;
2033
- const DEFAULT_MAX_MESSAGE_CHARACTERS = 8e3;
2034
- const TRUNCATION_PREFIX = "...\n";
2035
- class MessageBuffer {
2036
- messages = [];
2037
- listeners = [];
2038
- nextId = 1;
2039
- enabled;
2040
- maxMessages;
2041
- maxCharacters;
2042
- maxMessageCharacters;
2043
- constructor(options = {}) {
2044
- this.enabled = options.enabled ?? true;
2045
- this.maxMessages = Math.max(1, options.maxMessages ?? DEFAULT_MAX_MESSAGES);
2046
- this.maxCharacters = Math.max(1, options.maxCharacters ?? DEFAULT_MAX_CHARACTERS);
2047
- this.maxMessageCharacters = Math.max(1, options.maxMessageCharacters ?? DEFAULT_MAX_MESSAGE_CHARACTERS);
2048
- }
2049
- addMessage(content, type = "assistant") {
2050
- const id = `msg-${this.nextId++}`;
2051
- if (!this.enabled) {
2052
- return id;
2053
- }
2054
- const message = {
2055
- id,
2056
- timestamp: /* @__PURE__ */ new Date(),
2057
- content: this.normalizeContent(content),
2058
- type
1571
+ if (envelope.memberAgentId && envelope.memberAgentId !== taskContext.memberAgentId) {
1572
+ return {
1573
+ nextMetadata: metadata,
1574
+ dispatchAck: {
1575
+ outcome: "invalid",
1576
+ reason: "member_agent_id_mismatch",
1577
+ diagnostic: `Happy Org dispatch ack ignored: member_agent_id ${envelope.memberAgentId} does not match active member ${taskContext.memberAgentId}.`
1578
+ }
2059
1579
  };
2060
- this.messages.push(message);
2061
- this.trimMessages();
2062
- this.notifyListeners();
2063
- return message.id;
2064
1580
  }
2065
- updateMessage(id, content, options = {}) {
2066
- if (!this.enabled) {
2067
- return false;
2068
- }
2069
- const index = this.messages.findIndex((message) => message.id === id);
2070
- if (index === -1) {
2071
- return false;
2072
- }
2073
- const normalizedContent = this.normalizeContent(content);
2074
- const previous = this.messages[index];
2075
- this.messages[index] = {
2076
- ...previous,
2077
- content: this.truncateContent(options.mode === "replace" ? normalizedContent : previous.content + normalizedContent)
1581
+ if (replyContext.positionId && envelope.positionId && envelope.positionId !== replyContext.positionId) {
1582
+ return {
1583
+ nextMetadata: metadata,
1584
+ dispatchAck: {
1585
+ outcome: "invalid",
1586
+ reason: "position_id_mismatch",
1587
+ diagnostic: `Happy Org dispatch ack ignored: position_id ${envelope.positionId} does not match active position ${replyContext.positionId}.`
1588
+ }
2078
1589
  };
2079
- this.trimMessages();
2080
- this.notifyListeners();
2081
- return true;
2082
1590
  }
2083
- removeMessage(id) {
2084
- if (!this.enabled) {
2085
- return false;
2086
- }
2087
- const index = this.messages.findIndex((message) => message.id === id);
2088
- if (index === -1) {
2089
- return false;
2090
- }
2091
- this.messages.splice(index, 1);
2092
- this.notifyListeners();
2093
- return true;
1591
+ if (replyContext.responsibilityId && envelope.responsibilityId && envelope.responsibilityId !== replyContext.responsibilityId) {
1592
+ return {
1593
+ nextMetadata: metadata,
1594
+ dispatchAck: {
1595
+ outcome: "invalid",
1596
+ reason: "responsibility_id_mismatch",
1597
+ diagnostic: `Happy Org dispatch ack ignored: responsibility_id ${envelope.responsibilityId} does not match active responsibility ${replyContext.responsibilityId}.`
1598
+ }
1599
+ };
2094
1600
  }
2095
- /**
2096
- * Update the last message of a specific type by appending content to it
2097
- * Useful for streaming responses where deltas should accumulate in one message
2098
- */
2099
- updateLastMessage(contentDelta, type = "assistant") {
2100
- if (!this.enabled) {
2101
- return;
2102
- }
2103
- const normalizedDelta = this.normalizeContent(contentDelta);
2104
- for (let i = this.messages.length - 1; i >= 0; i--) {
2105
- if (this.messages[i].type === type) {
2106
- const oldMessage = this.messages[i];
2107
- const updatedMessage = {
2108
- ...oldMessage,
2109
- content: this.truncateContent(oldMessage.content + normalizedDelta)
2110
- };
2111
- this.messages[i] = updatedMessage;
2112
- this.trimMessages();
2113
- this.notifyListeners();
2114
- return;
1601
+ if (replyContext.routeType && envelope.routeType && envelope.routeType !== replyContext.routeType) {
1602
+ return {
1603
+ nextMetadata: metadata,
1604
+ dispatchAck: {
1605
+ outcome: "invalid",
1606
+ reason: "route_type_mismatch",
1607
+ diagnostic: `Happy Org dispatch ack ignored: route_type ${envelope.routeType} does not match active route ${replyContext.routeType}.`
2115
1608
  }
2116
- }
2117
- this.addMessage(normalizedDelta, type);
1609
+ };
2118
1610
  }
2119
- /**
2120
- * Remove the last message of a specific type
2121
- * Useful for removing placeholder messages like "Thinking..." when actual response starts
2122
- */
2123
- removeLastMessage(type) {
2124
- if (!this.enabled) {
2125
- return false;
2126
- }
2127
- for (let i = this.messages.length - 1; i >= 0; i--) {
2128
- if (this.messages[i].type === type) {
2129
- this.messages.splice(i, 1);
2130
- this.notifyListeners();
2131
- return true;
1611
+ if (replyContext.ackType && envelope.ackType && envelope.ackType !== replyContext.ackType) {
1612
+ return {
1613
+ nextMetadata: metadata,
1614
+ dispatchAck: {
1615
+ outcome: "invalid",
1616
+ reason: "ack_type_mismatch",
1617
+ diagnostic: `Happy Org dispatch ack ignored: ack_type ${envelope.ackType} does not match active ack_type ${replyContext.ackType}.`
2132
1618
  }
2133
- }
2134
- return false;
1619
+ };
2135
1620
  }
2136
- getMessages() {
2137
- return [...this.messages];
1621
+ if (replyContext.replyMode && envelope.replyMode && envelope.replyMode !== replyContext.replyMode) {
1622
+ return {
1623
+ nextMetadata: metadata,
1624
+ dispatchAck: {
1625
+ outcome: "invalid",
1626
+ reason: "reply_mode_mismatch",
1627
+ diagnostic: `Happy Org dispatch ack ignored: reply_mode ${envelope.replyMode} does not match active reply_mode ${replyContext.replyMode}.`
1628
+ }
1629
+ };
2138
1630
  }
2139
- clear() {
2140
- this.messages = [];
2141
- this.nextId = 1;
2142
- if (!this.enabled) {
2143
- return;
2144
- }
2145
- this.notifyListeners();
1631
+ const existingAck = currentHappyOrg.dispatchAcks?.[envelope.dispatchId];
1632
+ if (existingAck) {
1633
+ return {
1634
+ nextMetadata: metadata,
1635
+ dispatchAck: {
1636
+ outcome: "duplicate",
1637
+ reason: "already_submitted",
1638
+ diagnostic: `Happy Org dispatch ack skipped: dispatch ${envelope.dispatchId} was already acknowledged as ${existingAck.status}.`
1639
+ }
1640
+ };
2146
1641
  }
2147
- onUpdate(listener) {
2148
- if (!this.enabled) {
2149
- return () => {
2150
- };
2151
- }
2152
- this.listeners.push(listener);
2153
- return () => {
2154
- const index = this.listeners.indexOf(listener);
2155
- if (index > -1) {
2156
- this.listeners.splice(index, 1);
1642
+ if (!opts.submitDispatchAck) {
1643
+ return {
1644
+ nextMetadata: metadata,
1645
+ dispatchAck: {
1646
+ outcome: "error",
1647
+ reason: "submit_callback_unavailable",
1648
+ diagnostic: `Happy Org dispatch ack could not be submitted for ${envelope.dispatchId}: no submit callback is available.`
2157
1649
  }
2158
1650
  };
2159
1651
  }
2160
- normalizeContent(content) {
2161
- return this.truncateContent(formatDisplayMessage(content));
1652
+ try {
1653
+ await opts.submitDispatchAck({
1654
+ organizationId: taskContext.organizationId,
1655
+ dispatchId: envelope.dispatchId,
1656
+ memberAgentId: taskContext.memberAgentId,
1657
+ scope: envelope.scope,
1658
+ readAck: envelope.readAck,
1659
+ taskAck: envelope.taskAck,
1660
+ status: envelope.status,
1661
+ note: envelope.note
1662
+ });
1663
+ return {
1664
+ nextMetadata: recordHappyOrgDispatchAck(
1665
+ metadata,
1666
+ envelope,
1667
+ opts.now?.() ?? Date.now()
1668
+ ),
1669
+ dispatchAck: {
1670
+ outcome: "submitted",
1671
+ reason: "submitted",
1672
+ diagnostic: null
1673
+ }
1674
+ };
1675
+ } catch (error) {
1676
+ const diagnostic = `Happy Org dispatch ack failed for ${envelope.dispatchId}: ${normalizeDispatchAckError(error)}.`;
1677
+ logger.debug("[HappyOrg] Dispatch ack submission failed", {
1678
+ dispatchId: envelope.dispatchId,
1679
+ error
1680
+ });
1681
+ return {
1682
+ nextMetadata: metadata,
1683
+ dispatchAck: {
1684
+ outcome: "error",
1685
+ reason: "submit_failed",
1686
+ diagnostic
1687
+ }
1688
+ };
2162
1689
  }
2163
- truncateContent(content) {
2164
- if (content.length <= this.maxMessageCharacters) {
2165
- return content;
2166
- }
2167
- const tailLength = Math.max(0, this.maxMessageCharacters - TRUNCATION_PREFIX.length);
2168
- return `${TRUNCATION_PREFIX}${content.slice(content.length - tailLength)}`;
1690
+ }
1691
+ function deriveHomeSlug(path) {
1692
+ if (!path) {
1693
+ return null;
2169
1694
  }
2170
- trimMessages() {
2171
- while (this.messages.length > this.maxMessages) {
2172
- this.messages.shift();
2173
- }
2174
- let totalCharacters = this.messages.reduce((sum, message) => sum + message.content.length, 0);
2175
- while (totalCharacters > this.maxCharacters && this.messages.length > 1) {
2176
- const removed = this.messages.shift();
2177
- if (removed) {
2178
- totalCharacters -= removed.content.length;
2179
- }
2180
- }
1695
+ const trimmedPath = path.trim().replace(/[\\/]+$/, "");
1696
+ if (!trimmedPath) {
1697
+ return null;
2181
1698
  }
2182
- notifyListeners() {
2183
- const messages = this.getMessages();
2184
- this.listeners.forEach((listener) => listener(messages));
1699
+ return basename(trimmedPath) || trimmedPath;
1700
+ }
1701
+ function buildSpecialistHomeIdentity(opts) {
1702
+ const metadata = opts.metadata ?? null;
1703
+ const fallback = opts.fallback ?? null;
1704
+ const path = normalizeOptionalText(metadata?.path) ?? fallback?.path ?? null;
1705
+ const homeSlug = deriveHomeSlug(path) ?? fallback?.homeSlug ?? null;
1706
+ if (!homeSlug) {
1707
+ return fallback ? { ...fallback } : null;
2185
1708
  }
1709
+ return {
1710
+ homeSlug,
1711
+ path,
1712
+ happySessionId: normalizeOptionalText(opts.sessionId) ?? fallback?.happySessionId ?? null,
1713
+ machineId: normalizeOptionalText(metadata?.machineId) ?? fallback?.machineId ?? null,
1714
+ startedBy: metadata?.startedBy ?? fallback?.startedBy ?? null,
1715
+ flavor: normalizeOptionalText(metadata?.flavor) ?? fallback?.flavor ?? null
1716
+ };
2186
1717
  }
2187
-
2188
- function isRecord(value) {
2189
- return typeof value === "object" && value !== null && !Array.isArray(value);
1718
+ function withDefaultReplyRouting(replyContext) {
1719
+ return {
1720
+ ...replyContext,
1721
+ routeType: replyContext.routeType ?? "ack_plus_reply",
1722
+ ackType: replyContext.ackType ?? "dispatch_ack",
1723
+ replyMode: replyContext.replyMode ?? "reply-first"
1724
+ };
2190
1725
  }
2191
- function hasClaudeCompatibilityShadow(value) {
2192
- if (!isRecord(value)) {
2193
- return false;
1726
+ function resolveReplyContextFromMessage(message, taskContext) {
1727
+ const metaReplyContext = message.meta?.happyOrg?.replyContext;
1728
+ if (metaReplyContext?.dispatchId && metaReplyContext.scope && metaReplyContext.replyTo) {
1729
+ return withDefaultReplyRouting({
1730
+ dispatchId: metaReplyContext.dispatchId,
1731
+ taskId: metaReplyContext.taskId ?? taskContext?.taskId ?? null,
1732
+ organizationId: metaReplyContext.organizationId ?? taskContext?.organizationId ?? null,
1733
+ scope: metaReplyContext.scope,
1734
+ replyTo: metaReplyContext.replyTo,
1735
+ memberAgentId: metaReplyContext.memberAgentId ?? taskContext?.memberAgentId ?? null,
1736
+ sessionId: metaReplyContext.sessionId ?? null,
1737
+ positionId: metaReplyContext.positionId ?? taskContext?.positionId ?? null,
1738
+ responsibilityId: metaReplyContext.responsibilityId ?? taskContext?.responsibilityId ?? null,
1739
+ routeType: metaReplyContext.routeType ?? null,
1740
+ ackType: metaReplyContext.ackType ?? null,
1741
+ replyMode: metaReplyContext.replyMode ?? null,
1742
+ planIntent: metaReplyContext.planIntent ?? null,
1743
+ routerReason: metaReplyContext.routerReason ?? null,
1744
+ goldenRouteId: metaReplyContext.goldenRouteId ?? null
1745
+ });
2194
1746
  }
2195
- if (typeof value.happyCompatibilityShadow === "string" && value.happyCompatibilityShadow.trim()) {
2196
- return true;
1747
+ const fields = parseHappyOrgEnvelopeFields(message.content.text);
1748
+ const dispatchId = normalizeOptionalText(fields.dispatch_id);
1749
+ const scope = normalizeOptionalText(fields.scope);
1750
+ const replyTo = normalizeOptionalText(fields.reply_to);
1751
+ const taskId = normalizeOptionalText(fields.task_id);
1752
+ if (!dispatchId || !scope || !replyTo) {
1753
+ return null;
2197
1754
  }
2198
- const message = value.message;
2199
- if (!isRecord(message) || !Array.isArray(message.content)) {
2200
- return false;
1755
+ if (taskId && taskContext?.taskId && taskId !== taskContext.taskId) {
1756
+ return null;
2201
1757
  }
2202
- return message.content.some((block) => isRecord(block) && typeof block.happyCompatibilityShadow === "string" && block.happyCompatibilityShadow.trim().length > 0);
1758
+ return withDefaultReplyRouting({
1759
+ dispatchId,
1760
+ taskId: taskId ?? taskContext?.taskId ?? null,
1761
+ organizationId: normalizeOptionalText(fields.organization_id) ?? taskContext?.organizationId ?? null,
1762
+ scope,
1763
+ replyTo,
1764
+ memberAgentId: normalizeOptionalText(fields.member_agent_id) ?? normalizeOptionalText(fields.agent_id) ?? taskContext?.memberAgentId ?? null,
1765
+ sessionId: normalizeOptionalText(fields.session_id),
1766
+ positionId: normalizeOptionalText(fields.position_id) ?? taskContext?.positionId ?? null,
1767
+ responsibilityId: normalizeOptionalText(fields.responsibility_id) ?? taskContext?.responsibilityId ?? null,
1768
+ routeType: normalizeRouteType(fields.route_type),
1769
+ ackType: normalizeAckType(fields.ack_type),
1770
+ replyMode: normalizeReplyMode(fields.reply_mode),
1771
+ planIntent: normalizeOptionalText(fields.plan_intent),
1772
+ routerReason: normalizeOptionalText(fields.router_reason),
1773
+ goldenRouteId: normalizeOptionalText(fields.golden_route_id)
1774
+ });
2203
1775
  }
2204
- function createSessionTranscriptInkRenderer(opts) {
2205
- const streamedAssistantMessages = /* @__PURE__ */ new Map();
2206
- const renderBufferedText = (text, type, streamMetadata) => {
2207
- const normalizedText = text.trim();
2208
- if (!normalizedText) {
2209
- return;
2210
- }
2211
- const streamKey = typeof streamMetadata?.messageId === "string" ? streamMetadata.messageId : typeof streamMetadata?.id === "string" ? streamMetadata.id : null;
2212
- const phase = typeof streamMetadata?.phase === "string" ? streamMetadata.phase : null;
2213
- const updateMode = streamMetadata?.mode === "replace" || phase === "commit" ? "replace" : "append";
2214
- if (phase === "abort") {
2215
- if (streamKey) {
2216
- const bufferedId = streamedAssistantMessages.get(streamKey);
2217
- if (bufferedId) {
2218
- opts.messageBuffer.removeMessage(bufferedId);
2219
- streamedAssistantMessages.delete(streamKey);
2220
- }
2221
- }
2222
- return;
2223
- }
2224
- if (!streamKey) {
2225
- opts.messageBuffer.addMessage(normalizedText, type);
2226
- return;
2227
- }
2228
- const existingBufferedId = streamedAssistantMessages.get(streamKey);
2229
- if (!existingBufferedId) {
2230
- const nextBufferedId = opts.messageBuffer.addMessage(normalizedText, type);
2231
- if (phase !== "commit") {
2232
- streamedAssistantMessages.set(streamKey, nextBufferedId);
2233
- }
2234
- return;
2235
- }
2236
- const updated = opts.messageBuffer.updateMessage(existingBufferedId, normalizedText, {
2237
- mode: updateMode
2238
- });
2239
- if (!updated) {
2240
- const nextBufferedId = opts.messageBuffer.addMessage(normalizedText, type);
2241
- if (phase !== "commit") {
2242
- streamedAssistantMessages.set(streamKey, nextBufferedId);
2243
- }
2244
- return;
2245
- }
2246
- if (phase === "commit") {
2247
- streamedAssistantMessages.delete(streamKey);
2248
- }
2249
- };
2250
- const renderStructuredAgentPayload = (payload) => {
2251
- if (!isRecord(payload) || typeof payload.type !== "string") {
2252
- return;
2253
- }
2254
- switch (payload.type) {
2255
- case "message":
2256
- case "reasoning": {
2257
- if (typeof payload.message !== "string") {
2258
- return;
2259
- }
2260
- renderBufferedText(
2261
- payload.message,
2262
- payload.type === "reasoning" ? "status" : "assistant",
2263
- payload
2264
- );
2265
- return;
2266
- }
2267
- case "thinking": {
2268
- if (typeof payload.text === "string" && payload.text.trim()) {
2269
- opts.messageBuffer.addMessage(`[Thinking] ${payload.text.trim()}`, "status");
2270
- }
2271
- return;
2272
- }
2273
- case "tool-call": {
2274
- const name = typeof payload.name === "string" ? payload.name : typeof payload.toolName === "string" ? payload.toolName : "tool";
2275
- const inputPreview = truncateDisplayMessage(
2276
- isRecord(payload.input) || Array.isArray(payload.input) ? JSON.stringify(payload.input) : payload.input,
2277
- 120
2278
- );
2279
- opts.messageBuffer.addMessage(
2280
- `Executing: ${name}${inputPreview ? ` ${inputPreview}` : ""}`,
2281
- "tool"
2282
- );
2283
- return;
2284
- }
2285
- case "tool-result":
2286
- case "tool-call-result": {
2287
- const resultValue = Object.prototype.hasOwnProperty.call(payload, "output") ? payload.output : payload.result;
2288
- const resultPreview = truncateDisplayMessage(resultValue, 200);
2289
- const prefix = payload.isError ? "Error:" : "Result:";
2290
- opts.messageBuffer.addMessage(
2291
- resultPreview ? `${prefix} ${resultPreview}` : "Tool completed",
2292
- payload.isError ? "status" : "result"
2293
- );
2294
- return;
2295
- }
2296
- case "file-edit":
2297
- case "fs-edit": {
2298
- const description = typeof payload.description === "string" ? payload.description : "File edit";
2299
- opts.messageBuffer.addMessage(`File edit: ${description}`, "tool");
2300
- return;
2301
- }
2302
- case "terminal-output": {
2303
- if (typeof payload.data !== "string") {
2304
- return;
2305
- }
2306
- const preview = renderTerminalOutputPreview(payload.data);
2307
- if (preview) {
2308
- opts.messageBuffer.addMessage(preview, "result");
2309
- }
2310
- return;
2311
- }
2312
- case "permission-request": {
2313
- const toolName = typeof payload.toolName === "string" ? payload.toolName : "tool";
2314
- opts.messageBuffer.addMessage(`Permission requested: ${toolName}`, "status");
2315
- return;
2316
- }
2317
- case "exec-approval-request": {
2318
- opts.messageBuffer.addMessage("Exec approval requested", "status");
2319
- return;
2320
- }
2321
- case "patch-apply-begin": {
2322
- opts.messageBuffer.addMessage("Applying patch...", "tool");
2323
- return;
2324
- }
2325
- case "patch-apply-end": {
2326
- const completionMessage = payload.success === false ? truncateDisplayMessage(payload.stderr, 200) || "Patch failed" : truncateDisplayMessage(payload.stdout, 200) || "Patch applied";
2327
- opts.messageBuffer.addMessage(completionMessage, payload.success === false ? "status" : "result");
2328
- return;
2329
- }
2330
- case "event": {
2331
- if (payload.name === "thinking" && isRecord(payload.payload) && typeof payload.payload.text === "string" && payload.payload.text.trim()) {
2332
- opts.messageBuffer.addMessage(`[Thinking] ${payload.payload.text.trim()}`, "status");
2333
- }
2334
- return;
2335
- }
2336
- case "task_started":
2337
- case "task_complete":
2338
- case "turn_aborted":
2339
- case "turn-report":
2340
- case "token_count":
2341
- case "token-count":
2342
- return;
2343
- default:
2344
- return;
2345
- }
2346
- };
2347
- const renderLegacyClaudeOutput = (payload) => {
2348
- if (!isRecord(payload) || hasClaudeCompatibilityShadow(payload)) {
2349
- return;
2350
- }
2351
- if (payload.type === "summary" && typeof payload.summary === "string" && payload.summary.trim()) {
2352
- opts.messageBuffer.addMessage(payload.summary.trim(), "result");
2353
- return;
2354
- }
2355
- if (payload.type !== "assistant") {
2356
- return;
2357
- }
2358
- const message = isRecord(payload.message) ? payload.message : null;
2359
- const content = message?.content;
2360
- if (typeof content === "string" && content.trim()) {
2361
- opts.messageBuffer.addMessage(content.trim(), "assistant");
2362
- return;
2363
- }
2364
- if (!Array.isArray(content)) {
2365
- return;
2366
- }
2367
- for (const block of content) {
2368
- if (!isRecord(block) || typeof block.type !== "string") {
2369
- continue;
2370
- }
2371
- if (block.type === "thinking" && typeof block.thinking === "string" && block.thinking.trim()) {
2372
- opts.messageBuffer.addMessage(`[Thinking] ${block.thinking.trim()}`, "status");
2373
- continue;
2374
- }
2375
- if (block.type === "text" && typeof block.text === "string" && block.text.trim()) {
2376
- opts.messageBuffer.addMessage(block.text.trim(), "assistant");
2377
- continue;
2378
- }
2379
- if (block.type === "tool_use") {
2380
- const toolName = typeof block.name === "string" ? block.name : "tool";
2381
- const inputPreview = truncateDisplayMessage(
2382
- isRecord(block.input) || Array.isArray(block.input) ? JSON.stringify(block.input) : block.input,
2383
- 120
2384
- );
2385
- opts.messageBuffer.addMessage(
2386
- `Executing: ${toolName}${inputPreview ? ` ${inputPreview}` : ""}`,
2387
- "tool"
2388
- );
2389
- }
2390
- }
2391
- };
2392
- const renderSessionEvent = (payload) => {
2393
- if (!isRecord(payload) || typeof payload.type !== "string") {
2394
- return;
2395
- }
2396
- switch (payload.type) {
2397
- case "message":
2398
- if (typeof payload.message === "string" && payload.message.trim()) {
2399
- opts.messageBuffer.addMessage(payload.message.trim(), "status");
2400
- }
2401
- return;
2402
- case "switch":
2403
- if (payload.mode === "local" || payload.mode === "remote") {
2404
- opts.messageBuffer.addMessage(`Mode switched to ${payload.mode}`, "status");
2405
- }
2406
- return;
2407
- case "permission-mode-changed":
2408
- if (typeof payload.mode === "string" && payload.mode.trim()) {
2409
- opts.messageBuffer.addMessage(`Permission mode: ${payload.mode}`, "status");
2410
- }
2411
- return;
2412
- case "ready":
2413
- return;
2414
- default:
2415
- return;
2416
- }
2417
- };
2418
- return (transcriptMessage) => {
2419
- if (transcriptMessage.message.role === "user") {
2420
- const text = transcriptMessage.message.content.text.trim();
2421
- if (text) {
2422
- opts.messageBuffer.addMessage(text, "user");
2423
- }
2424
- return;
2425
- }
2426
- switch (transcriptMessage.message.content.type) {
2427
- case "output":
2428
- renderLegacyClaudeOutput(transcriptMessage.message.content.data);
2429
- return;
2430
- case "codex":
2431
- renderStructuredAgentPayload(transcriptMessage.message.content.data);
2432
- return;
2433
- case "acp":
2434
- renderStructuredAgentPayload(transcriptMessage.message.content.data);
2435
- return;
2436
- case "event":
2437
- renderSessionEvent(transcriptMessage.message.content.data);
2438
- return;
2439
- default:
2440
- opts.messageBuffer.addMessage(formatDisplayMessage(transcriptMessage.message), "status");
2441
- }
1776
+ function buildTerminatedStatusMessage(taskId) {
1777
+ return `Task ${taskId} is terminated. Reopen it with new context, a new decision, or a new resource before continuing.`;
1778
+ }
1779
+ function buildMemberBusyStatusMessage(memberAgentId, activeTaskId, nextTaskId) {
1780
+ return `Member ${memberAgentId} is already busy with active task ${activeTaskId}. Reject task ${nextTaskId} without continuing token usage.`;
1781
+ }
1782
+ function buildOwnerConflictStatusMessage(taskId, ownerAgentId) {
1783
+ return `Task ${taskId} is already active under owner ${ownerAgentId}. This turn must exit without continuing token usage.`;
1784
+ }
1785
+ function inferDraftTurnStatus(draft) {
1786
+ if (draft?.turnStatus === "turn_update" || draft?.turnStatus === "task_complete") {
1787
+ return draft.turnStatus;
1788
+ }
1789
+ return null;
1790
+ }
1791
+ function inferInterventionType(draft) {
1792
+ if (draft?.interventionType === "none" || draft?.interventionType === "review_needed" || draft?.interventionType === "blocker" || draft?.interventionType === "decision_needed") {
1793
+ return draft.interventionType;
1794
+ }
1795
+ if (normalizeOptionalText(draft?.decisionNeeded)) {
1796
+ return "decision_needed";
1797
+ }
1798
+ if (normalizeOptionalText(draft?.blockerCode)) {
1799
+ return "blocker";
1800
+ }
1801
+ return "none";
1802
+ }
1803
+ function buildFallbackSummary(text, turnStatus) {
1804
+ const trimmed = text.trim();
1805
+ if (trimmed) {
1806
+ return trimmed.replace(/\s+/g, " ").slice(0, HAPPY_ORG_SUMMARY_MAX_LENGTH);
1807
+ }
1808
+ return turnStatus === "turn_aborted" ? "Turn aborted before completion." : "Turn completed without a textual summary.";
1809
+ }
1810
+ function normalizeTurnReportDraft(draft) {
1811
+ return {
1812
+ turnStatus: inferDraftTurnStatus(draft),
1813
+ summary: normalizeSummaryText(draft?.summary),
1814
+ interventionType: inferInterventionType(draft),
1815
+ blockerCode: normalizeOptionalText(draft?.blockerCode),
1816
+ decisionNeeded: normalizeOptionalText(draft?.decisionNeeded),
1817
+ targetArtifact: normalizePreviewableArtifactTarget(draft?.targetArtifact),
1818
+ accessChannelState: normalizeAccessChannelState(draft?.accessChannelState)
2442
1819
  };
2443
1820
  }
2444
-
2445
- let ConversationHistory$1 = class ConversationHistory {
2446
- messages = [];
2447
- maxMessages;
2448
- maxCharacters;
2449
- constructor(options = {}) {
2450
- this.maxMessages = options.maxMessages ?? 20;
2451
- this.maxCharacters = options.maxCharacters ?? 5e4;
2452
- }
2453
- isDuplicate(role, content) {
2454
- if (this.messages.length === 0) {
2455
- return false;
2456
- }
2457
- for (let index = this.messages.length - 1; index >= 0; index -= 1) {
2458
- const message = this.messages[index];
2459
- if (message.role !== role) {
2460
- continue;
2461
- }
2462
- const normalizedIncoming = content.trim().replace(/\s+/g, " ");
2463
- const normalizedExisting = message.content.replace(/\s+/g, " ");
2464
- return normalizedIncoming === normalizedExisting;
2465
- }
2466
- return false;
2467
- }
2468
- addUserMessage(content) {
2469
- this.addMessage("user", content);
2470
- }
2471
- addAssistantMessage(content) {
2472
- this.addMessage("assistant", content);
2473
- }
2474
- hasHistory() {
2475
- return this.messages.length > 0;
2476
- }
2477
- size() {
2478
- return this.messages.length;
2479
- }
2480
- clear() {
2481
- this.messages = [];
2482
- logger.debug("[ConversationHistory] History cleared");
2483
- }
2484
- getContextForNewSession(prefixMessage = "Continue from the prior session using the conversation below as context.") {
2485
- if (this.messages.length === 0) {
2486
- return "";
2487
- }
2488
- const formattedMessages = this.messages.map((message) => {
2489
- const role = message.role === "user" ? "User" : "Assistant";
2490
- const content = message.content.length > 2e3 ? `${message.content.slice(0, 2e3)}... [truncated]` : message.content;
2491
- return `${role}: ${content}`;
2492
- }).join("\n\n");
2493
- return [
2494
- "[PREVIOUS CONVERSATION CONTEXT]",
2495
- prefixMessage,
2496
- "",
2497
- formattedMessages,
2498
- "",
2499
- "[END OF PREVIOUS CONTEXT]",
2500
- ""
2501
- ].join("\n");
2502
- }
2503
- getSummary() {
2504
- const totalChars = this.messages.reduce((sum, message) => sum + message.content.length, 0);
2505
- const userCount = this.messages.filter((message) => message.role === "user").length;
2506
- const assistantCount = this.messages.filter((message) => message.role === "assistant").length;
2507
- return `${this.messages.length} messages (${userCount} user, ${assistantCount} assistant), ${totalChars} chars`;
2508
- }
2509
- addMessage(role, content) {
2510
- const trimmedContent = content.trim();
2511
- if (!trimmedContent) {
2512
- return;
2513
- }
2514
- if (this.isDuplicate(role, trimmedContent)) {
2515
- logger.debug(`[ConversationHistory] Skipping duplicate ${role} message (${trimmedContent.length} chars)`);
2516
- return;
2517
- }
2518
- this.messages.push({
2519
- role,
2520
- content: trimmedContent,
2521
- timestamp: Date.now()
2522
- });
2523
- this.trimHistory();
2524
- logger.debug(`[ConversationHistory] Added ${role} message (${trimmedContent.length} chars), total: ${this.messages.length}`);
1821
+ function stripCodeFence(text) {
1822
+ return text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
1823
+ }
1824
+ function extractTaggedTurnReport(text) {
1825
+ const matcher = new RegExp(
1826
+ `<${HAPPY_ORG_TURN_REPORT_TAG}>\\s*([\\s\\S]*?)\\s*</${HAPPY_ORG_TURN_REPORT_TAG}>`,
1827
+ "gi"
1828
+ );
1829
+ let lastMatch = null;
1830
+ for (let match = matcher.exec(text); match; match = matcher.exec(text)) {
1831
+ lastMatch = match;
2525
1832
  }
2526
- trimHistory() {
2527
- while (this.messages.length > this.maxMessages) {
2528
- this.messages.shift();
2529
- }
2530
- let totalChars = this.messages.reduce((sum, message) => sum + message.content.length, 0);
2531
- while (totalChars > this.maxCharacters && this.messages.length > 1) {
2532
- const removed = this.messages.shift();
2533
- if (removed) {
2534
- totalChars -= removed.content.length;
2535
- }
2536
- }
1833
+ if (!lastMatch) {
1834
+ return {
1835
+ cleanedText: text.trim(),
1836
+ draft: null
1837
+ };
2537
1838
  }
2538
- };
2539
-
2540
- const INTERACTION_SUPERSEDED_ERROR = "Interaction superseded by new user message";
2541
- const INTERACTION_TIMED_OUT_ERROR = "Interaction timed out waiting for user response";
2542
- const DEFAULT_INTERACTION_TIMEOUT_MS = 2 * 60 * 1e3;
2543
- function getPendingInteractionTimeoutMs() {
2544
- const raw = Number(process.env.HAPPY_INTERACTION_TIMEOUT_MS);
2545
- if (Number.isFinite(raw) && raw > 0) {
2546
- return raw;
1839
+ const rawBlock = stripCodeFence(lastMatch[1] ?? "");
1840
+ let draft = null;
1841
+ try {
1842
+ const parsed = JSON.parse(rawBlock);
1843
+ draft = {
1844
+ turnStatus: normalizeOptionalText(parsed.turnStatus),
1845
+ summary: normalizeSummaryText(parsed.summary),
1846
+ interventionType: normalizeOptionalText(parsed.interventionType),
1847
+ blockerCode: normalizeOptionalText(parsed.blockerCode),
1848
+ decisionNeeded: normalizeOptionalText(parsed.decisionNeeded),
1849
+ targetArtifact: normalizePreviewableArtifactTarget(parsed.targetArtifact),
1850
+ accessChannelState: normalizeAccessChannelState(parsed.accessChannelState)
1851
+ };
1852
+ } catch {
1853
+ draft = null;
2547
1854
  }
2548
- return DEFAULT_INTERACTION_TIMEOUT_MS;
1855
+ const cleanedText = `${text.slice(0, lastMatch.index)}${text.slice(lastMatch.index + lastMatch[0].length)}`.replace(/\n{3,}/g, "\n\n").trim();
1856
+ return {
1857
+ cleanedText,
1858
+ draft
1859
+ };
2549
1860
  }
2550
- class BasePermissionHandler {
2551
- pendingRequests = /* @__PURE__ */ new Map();
2552
- session;
2553
- isResetting = false;
2554
- constructor(session) {
2555
- this.session = session;
2556
- this.setupRpcHandler();
2557
- }
2558
- /**
2559
- * Update the session reference (used after offline reconnection swaps sessions).
2560
- * This is critical for avoiding stale session references after onSessionSwap.
2561
- */
2562
- updateSession(newSession) {
2563
- logger.debug(`${this.getLogPrefix()} Session reference updated`);
2564
- this.session = newSession;
2565
- this.setupRpcHandler();
1861
+ function buildRepeatFingerprint(context, blockerCode, targetArtifact) {
1862
+ if (!blockerCode) {
1863
+ return null;
2566
1864
  }
2567
- /**
2568
- * Setup RPC handler for permission responses.
2569
- */
2570
- setupRpcHandler() {
2571
- this.session.rpcHandlerManager.registerHandler(
2572
- "permission",
2573
- async (response) => {
2574
- const pending = this.pendingRequests.get(response.id);
2575
- if (!pending) {
2576
- logger.debug(`${this.getLogPrefix()} Permission request not found or already resolved`);
2577
- return;
2578
- }
2579
- this.pendingRequests.delete(response.id);
2580
- this.clearPendingRequestTimeout(pending);
2581
- const result = response.approved ? { decision: response.decision === "approved_for_session" ? "approved_for_session" : "approved" } : { decision: response.decision === "denied" ? "denied" : "abort" };
2582
- pending.resolve(result);
2583
- this.session.updateAgentState((currentState) => {
2584
- const request = currentState.requests?.[response.id];
2585
- if (!request) return currentState;
2586
- const { [response.id]: _, ...remainingRequests } = currentState.requests || {};
2587
- let res = {
2588
- ...currentState,
2589
- requests: remainingRequests,
2590
- completedRequests: {
2591
- ...currentState.completedRequests,
2592
- [response.id]: {
2593
- ...request,
2594
- completedAt: Date.now(),
2595
- status: response.approved ? "approved" : "denied",
2596
- decision: result.decision
2597
- }
2598
- }
2599
- };
2600
- return res;
2601
- });
2602
- logger.debug(`${this.getLogPrefix()} Permission ${response.approved ? "approved" : "denied"} for ${pending.toolName}`);
2603
- }
2604
- );
1865
+ return [
1866
+ context.taskId,
1867
+ context.memberAgentId,
1868
+ blockerCode,
1869
+ targetArtifact ?? ""
1870
+ ].join("::");
1871
+ }
1872
+ function resolveReportedTurnStatus(transportTurnStatus, draft) {
1873
+ if (transportTurnStatus === "turn_aborted") {
1874
+ return "turn_aborted";
2605
1875
  }
2606
- /**
2607
- * Add a pending request to the agent state.
2608
- */
2609
- addPendingRequestToState(toolCallId, toolName, input) {
2610
- this.session.updateAgentState((currentState) => ({
2611
- ...currentState,
2612
- requests: {
2613
- ...currentState.requests,
2614
- [toolCallId]: {
2615
- tool: toolName,
2616
- arguments: input,
2617
- createdAt: Date.now()
2618
- }
2619
- }
2620
- }));
1876
+ return draft.turnStatus === "task_complete" ? "task_complete" : "turn_update";
1877
+ }
1878
+ function buildRuntimeStateAfterTurn(report) {
1879
+ if (report.turnStatus === "task_complete") {
1880
+ return {
1881
+ status: "waiting_close",
1882
+ reason: "awaiting_ceo_close"
1883
+ };
2621
1884
  }
2622
- registerPendingRequest(toolCallId, toolName, input, logSuffix = "") {
2623
- return new Promise((resolve, reject) => {
2624
- const pending = {
2625
- resolve,
2626
- reject,
2627
- toolName,
2628
- input
1885
+ switch (report.interventionType) {
1886
+ case "decision_needed":
1887
+ return {
1888
+ status: "waiting_decision",
1889
+ reason: "awaiting_user_decision"
1890
+ };
1891
+ case "review_needed":
1892
+ return {
1893
+ status: "waiting_review",
1894
+ reason: "awaiting_ceo_review"
1895
+ };
1896
+ case "blocker":
1897
+ return {
1898
+ status: "waiting_review",
1899
+ reason: "awaiting_ceo_context"
1900
+ };
1901
+ default:
1902
+ return {
1903
+ status: "active",
1904
+ reason: null
2629
1905
  };
2630
- pending.timeoutHandle = setTimeout(() => {
2631
- this.handlePendingRequestTimeout(toolCallId, pending);
2632
- }, getPendingInteractionTimeoutMs());
2633
- this.pendingRequests.set(toolCallId, pending);
2634
- this.addPendingRequestToState(toolCallId, toolName, input);
2635
- logger.debug(`${this.getLogPrefix()} Permission request sent for tool: ${toolName} (${toolCallId})${logSuffix}`);
2636
- });
2637
- }
2638
- hasPendingRequests() {
2639
- return this.pendingRequests.size > 0;
2640
1906
  }
2641
- supersedePendingRequests(reason = INTERACTION_SUPERSEDED_ERROR) {
2642
- const pendingSnapshot = Array.from(this.pendingRequests.entries());
2643
- if (pendingSnapshot.length === 0) {
2644
- return 0;
2645
- }
2646
- this.pendingRequests.clear();
2647
- const completedAt = Date.now();
2648
- for (const [, pending] of pendingSnapshot) {
2649
- this.clearPendingRequestTimeout(pending);
2650
- pending.resolve({ decision: "abort" });
2651
- }
2652
- this.session.updateAgentState((currentState) => {
2653
- const requests = { ...currentState.requests || {} };
2654
- const completedRequests = { ...currentState.completedRequests || {} };
2655
- for (const [id, request] of Object.entries(requests)) {
2656
- if (request.requestKind === "selection") {
2657
- continue;
2658
- }
2659
- completedRequests[id] = {
2660
- ...request,
2661
- completedAt,
2662
- status: "denied",
2663
- reason,
2664
- decision: "abort",
2665
- requestKind: request.requestKind || "permission"
2666
- };
2667
- delete requests[id];
2668
- }
2669
- return {
2670
- ...currentState,
2671
- requests,
2672
- completedRequests
2673
- };
2674
- });
2675
- logger.debug(`${this.getLogPrefix()} Superseded ${pendingSnapshot.length} pending permission request(s)`);
2676
- return pendingSnapshot.length;
1907
+ }
1908
+ function buildHappyOrgTurnPrompt(prompt, turn) {
1909
+ const reopenLines = turn.reopenContext ? [
1910
+ "",
1911
+ "This task was explicitly reopened for this turn with the following new inputs:",
1912
+ turn.reopenContext.newContext ? `- newContext: ${turn.reopenContext.newContext}` : null,
1913
+ turn.reopenContext.newDecision ? `- newDecision: ${turn.reopenContext.newDecision}` : null,
1914
+ turn.reopenContext.newResource ? `- newResource: ${turn.reopenContext.newResource}` : null
1915
+ ].filter(Boolean) : [];
1916
+ const replyContextLines = turn.replyContext ? [
1917
+ "",
1918
+ "This turn also carries a formal dispatch reply context. If you acknowledge or update that dispatch, keep these values stable:",
1919
+ `dispatch_id=${turn.replyContext.dispatchId}`,
1920
+ turn.replyContext.organizationId ? `organization_id=${turn.replyContext.organizationId}` : null,
1921
+ turn.replyContext.taskId ? `task_id=${turn.replyContext.taskId}` : null,
1922
+ `scope=${turn.replyContext.scope}`,
1923
+ `reply_to=${turn.replyContext.replyTo}`,
1924
+ turn.replyContext.memberAgentId ? `member_agent_id=${turn.replyContext.memberAgentId}` : null,
1925
+ turn.replyContext.sessionId ? `session_id=${turn.replyContext.sessionId}` : null,
1926
+ turn.replyContext.positionId ? `position_id=${turn.replyContext.positionId}` : null,
1927
+ turn.replyContext.responsibilityId ? `responsibility_id=${turn.replyContext.responsibilityId}` : null,
1928
+ turn.replyContext.routeType ? `route_type=${turn.replyContext.routeType}` : null,
1929
+ turn.replyContext.ackType ? `ack_type=${turn.replyContext.ackType}` : null,
1930
+ turn.replyContext.replyMode ? `reply_mode=${turn.replyContext.replyMode}` : null,
1931
+ "If the route feels ambiguous or a routing skill misses, stay reply-first / ack_plus_reply and do not start repo or code analysis by default.",
1932
+ "When you send the formal dispatch business ack in your visible response, include exactly one raw key=value block (no markdown code fence) using this shape:",
1933
+ `ack_version=${HAPPY_ORG_REPLY_ACK_VERSION}`,
1934
+ `dispatch_id=${turn.replyContext.dispatchId}`,
1935
+ turn.replyContext.organizationId ? `organization_id=${turn.replyContext.organizationId}` : null,
1936
+ turn.replyContext.taskId ? `task_id=${turn.replyContext.taskId}` : null,
1937
+ `scope=${turn.replyContext.scope}`,
1938
+ `member_agent_id=${turn.replyContext.memberAgentId ?? turn.context.memberAgentId}`,
1939
+ turn.replyContext.sessionId ? `session_id=${turn.replyContext.sessionId}` : null,
1940
+ turn.replyContext.positionId ? `position_id=${turn.replyContext.positionId}` : null,
1941
+ turn.replyContext.responsibilityId ? `responsibility_id=${turn.replyContext.responsibilityId}` : null,
1942
+ `route_type=${turn.replyContext.routeType ?? "ack_plus_reply"}`,
1943
+ `ack_type=${turn.replyContext.ackType ?? "dispatch_ack"}`,
1944
+ `reply_mode=${turn.replyContext.replyMode ?? "reply-first"}`,
1945
+ `task_ack=${turn.context.taskId}`,
1946
+ "read_ack=yes",
1947
+ "status=accepted | standby | blocked",
1948
+ "note=<short note>",
1949
+ "Keep dispatch_id / task_id / position_id / responsibility_id stable across the visible ack, visible reply, and final turn report.",
1950
+ "When turnStatus=task_complete and reply context exists, include a short source reply for the original requester at reply_to before the final JSON block.",
1951
+ "That source reply / acceptance handoff mini-pack should explicitly mention position_status, latest_user_visible_result, blocker_summary, and acceptance_state=awaiting_acceptance.",
1952
+ "Use acceptance_state=closed only after CEO or the original requester actually confirms close; do not self-mark closed."
1953
+ ].filter(Boolean) : [];
1954
+ const specialistHomeLines = turn.specialistHome ? [
1955
+ "",
1956
+ "Reply from this specialist home identity when applicable:",
1957
+ `home_slug=${turn.specialistHome.homeSlug}`,
1958
+ turn.specialistHome.happySessionId ? `happy_session_id=${turn.specialistHome.happySessionId}` : null,
1959
+ turn.specialistHome.machineId ? `machine_id=${turn.specialistHome.machineId}` : null
1960
+ ].filter(Boolean) : [];
1961
+ const header = [
1962
+ "[HAPPY_ORG_TASK_CONTEXT]",
1963
+ `taskId=${turn.context.taskId}`,
1964
+ `organizationId=${turn.context.organizationId}`,
1965
+ turn.context.organizationRootPath ? `organizationRootPath=${turn.context.organizationRootPath}` : null,
1966
+ `memberAgentId=${turn.context.memberAgentId}`,
1967
+ `supervisorAgentId=${turn.context.supervisorAgentId}`,
1968
+ turn.context.positionId ? `positionId=${turn.context.positionId}` : null,
1969
+ turn.context.responsibilityId ? `responsibilityId=${turn.context.responsibilityId}` : null,
1970
+ "Stay on this exact task for the whole turn.",
1971
+ "End your response with exactly one raw JSON block inside these tags and do not wrap it in a markdown code fence:",
1972
+ `<${HAPPY_ORG_TURN_REPORT_TAG}>{"turnStatus":"turn_update","summary":"short task-board summary","interventionType":"none","blockerCode":null,"decisionNeeded":null,"targetArtifact":null,"accessChannelState":"ok"}</${HAPPY_ORG_TURN_REPORT_TAG}>`,
1973
+ "Allowed turnStatus values in the JSON block: turn_update, task_complete.",
1974
+ "Allowed interventionType values: none, review_needed, blocker, decision_needed.",
1975
+ "Allowed accessChannelState values: ok, reattach_required, runtime_replaced.",
1976
+ "Use turnStatus=task_complete only when you believe the assigned task is finished and ready for CEO acceptance.",
1977
+ "If turnStatus=task_complete, the task will wait for CEO close; do not treat it as automatically closed.",
1978
+ "If turnStatus=task_complete, interventionType must be review_needed.",
1979
+ "targetArtifact must be null or a concrete previewable file target. Use a repo-relative path, absolute path, file:// URL, or https:// URL when you actually produced a material or artifact the client should open directly.",
1980
+ "Do not use abstract labels like release-checklist-template or completion-board-template in targetArtifact.",
1981
+ `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.`,
1982
+ "Use blocker only for problems the organization may still solve internally.",
1983
+ "Use decision_needed only when a CEO or user decision is required.",
1984
+ "Use review_needed when supervisor review is needed but the work is not blocked.",
1985
+ ...reopenLines,
1986
+ ...replyContextLines,
1987
+ ...specialistHomeLines,
1988
+ "[/HAPPY_ORG_TASK_CONTEXT]",
1989
+ "",
1990
+ prompt
1991
+ ].filter(Boolean);
1992
+ return header.join("\n");
1993
+ }
1994
+ function resolveHappyOrgQueuedTurn(opts) {
1995
+ const metadata = opts.metadata ?? null;
1996
+ if (!metadata) {
1997
+ return {
1998
+ nextMetadata: null,
1999
+ queuedTurn: null,
2000
+ blocked: false
2001
+ };
2677
2002
  }
2678
- /**
2679
- * Reset state for new sessions.
2680
- * This method is idempotent - safe to call multiple times.
2681
- */
2682
- reset() {
2683
- if (this.isResetting) {
2684
- logger.debug(`${this.getLogPrefix()} Reset already in progress, skipping`);
2685
- return;
2686
- }
2687
- this.isResetting = true;
2688
- try {
2689
- const pendingSnapshot = Array.from(this.pendingRequests.entries());
2690
- this.pendingRequests.clear();
2691
- for (const [id, pending] of pendingSnapshot) {
2692
- try {
2693
- this.clearPendingRequestTimeout(pending);
2694
- pending.reject(new Error("Session reset"));
2695
- } catch (err) {
2696
- logger.debug(`${this.getLogPrefix()} Error rejecting pending request ${id}:`, err);
2697
- }
2698
- }
2699
- this.session.updateAgentState((currentState) => {
2700
- const pendingRequests = currentState.requests || {};
2701
- const completedRequests = { ...currentState.completedRequests };
2702
- for (const [id, request] of Object.entries(pendingRequests)) {
2703
- completedRequests[id] = {
2704
- ...request,
2705
- completedAt: Date.now(),
2706
- status: "canceled",
2707
- reason: "Session reset"
2708
- };
2709
- }
2003
+ const currentHappyOrg = normalizeHappyOrgMetadata(metadata);
2004
+ let nextHappyOrg = cloneHappyOrgMetadata(currentHappyOrg);
2005
+ const messageHappyOrg = opts.message.meta?.happyOrg;
2006
+ const now = opts.now?.() ?? Date.now();
2007
+ const createRunId = opts.createRunId ?? ((taskContext2, currentNow) => `${taskContext2.taskId}:${taskContext2.memberAgentId}:${currentNow}`);
2008
+ const specialistHome = buildSpecialistHomeIdentity({
2009
+ metadata,
2010
+ sessionId: opts.sessionId,
2011
+ fallback: currentHappyOrg.specialistHome
2012
+ });
2013
+ nextHappyOrg.specialistHome = cloneHappyOrgSpecialistHomeIdentity(specialistHome);
2014
+ if (messageHappyOrg?.taskContext) {
2015
+ const currentTaskId = currentHappyOrg.taskContext?.taskId ?? null;
2016
+ if (currentTaskId !== messageHappyOrg.taskContext.taskId) {
2017
+ if (currentHappyOrg.taskContext && currentHappyOrg.runtime?.status === "active") {
2710
2018
  return {
2711
- ...currentState,
2712
- requests: {},
2713
- completedRequests
2019
+ nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
2020
+ queuedTurn: null,
2021
+ blocked: true,
2022
+ statusMessage: buildMemberBusyStatusMessage(
2023
+ currentHappyOrg.taskContext.memberAgentId,
2024
+ currentHappyOrg.taskContext.taskId,
2025
+ messageHappyOrg.taskContext.taskId
2026
+ )
2714
2027
  };
2715
- });
2716
- logger.debug(`${this.getLogPrefix()} Permission handler reset`);
2717
- } finally {
2718
- this.isResetting = false;
2719
- }
2720
- }
2721
- clearPendingRequestTimeout(pending) {
2722
- if (pending?.timeoutHandle) {
2723
- clearTimeout(pending.timeoutHandle);
2724
- pending.timeoutHandle = void 0;
2725
- }
2726
- }
2727
- handlePendingRequestTimeout(toolCallId, pending) {
2728
- const active = this.pendingRequests.get(toolCallId);
2729
- if (!active || active !== pending) {
2730
- return;
2731
- }
2732
- this.pendingRequests.delete(toolCallId);
2733
- this.clearPendingRequestTimeout(active);
2734
- active.resolve({ decision: "abort" });
2735
- this.session.updateAgentState((currentState) => {
2736
- const request = currentState.requests?.[toolCallId] || {
2737
- tool: active.toolName,
2738
- arguments: active.input,
2739
- createdAt: Date.now(),
2740
- requestKind: "permission"
2741
- };
2742
- const { [toolCallId]: _, ...remainingRequests } = currentState.requests || {};
2743
- return {
2744
- ...currentState,
2745
- requests: remainingRequests,
2746
- completedRequests: {
2747
- ...currentState.completedRequests,
2748
- [toolCallId]: {
2749
- ...request,
2750
- completedAt: Date.now(),
2751
- status: "canceled",
2752
- reason: INTERACTION_TIMED_OUT_ERROR,
2753
- decision: "abort",
2754
- requestKind: request.requestKind || "permission"
2755
- }
2756
- }
2757
- };
2758
- });
2759
- this.session.sendSessionEvent({
2760
- type: "message",
2761
- message: "Pending interaction timed out waiting for a response. Send a new message to continue."
2762
- });
2763
- logger.debug(`${this.getLogPrefix()} Permission request timed out for ${active.toolName} (${toolCallId})`);
2764
- }
2765
- }
2766
-
2767
- class MessageQueue2 {
2768
- queue = [];
2769
- // Made public for testing
2770
- waiter = null;
2771
- closed = false;
2772
- onMessageHandler = null;
2773
- modeHasher;
2774
- constructor(modeHasher, onMessageHandler = null) {
2775
- this.modeHasher = modeHasher;
2776
- this.onMessageHandler = onMessageHandler;
2777
- logger.debug(`[MessageQueue2] Initialized`);
2778
- }
2779
- /**
2780
- * Set a handler that will be called when a message arrives
2781
- */
2782
- setOnMessage(handler) {
2783
- this.onMessageHandler = handler;
2784
- }
2785
- /**
2786
- * Push a message to the queue with a mode.
2787
- */
2788
- push(message, mode) {
2789
- if (this.closed) {
2790
- throw new Error("Cannot push to closed queue");
2791
- }
2792
- const modeHash = this.modeHasher(mode);
2793
- logger.debug(`[MessageQueue2] push() called with mode hash: ${modeHash}`);
2794
- this.queue.push({
2795
- message,
2796
- mode,
2797
- modeHash,
2798
- isolate: false
2799
- });
2800
- if (this.onMessageHandler) {
2801
- this.onMessageHandler(message, mode);
2802
- }
2803
- if (this.waiter) {
2804
- logger.debug(`[MessageQueue2] Notifying waiter`);
2805
- const waiter = this.waiter;
2806
- this.waiter = null;
2807
- waiter(true);
2808
- }
2809
- logger.debug(`[MessageQueue2] push() completed. Queue size: ${this.queue.length}`);
2810
- }
2811
- /**
2812
- * Push a message immediately without batching delay.
2813
- * Does not clear the queue or enforce isolation.
2814
- */
2815
- pushImmediate(message, mode) {
2816
- if (this.closed) {
2817
- throw new Error("Cannot push to closed queue");
2818
- }
2819
- const modeHash = this.modeHasher(mode);
2820
- logger.debug(`[MessageQueue2] pushImmediate() called with mode hash: ${modeHash}`);
2821
- this.queue.push({
2822
- message,
2823
- mode,
2824
- modeHash,
2825
- isolate: false
2826
- });
2827
- if (this.onMessageHandler) {
2828
- this.onMessageHandler(message, mode);
2829
- }
2830
- if (this.waiter) {
2831
- logger.debug(`[MessageQueue2] Notifying waiter for immediate message`);
2832
- const waiter = this.waiter;
2833
- this.waiter = null;
2834
- waiter(true);
2835
- }
2836
- logger.debug(`[MessageQueue2] pushImmediate() completed. Queue size: ${this.queue.length}`);
2837
- }
2838
- /**
2839
- * Push a message that must be processed in complete isolation.
2840
- * Clears any pending messages and ensures this message is never batched with others.
2841
- * Used for special commands that require dedicated processing.
2842
- */
2843
- pushIsolateAndClear(message, mode) {
2844
- if (this.closed) {
2845
- throw new Error("Cannot push to closed queue");
2846
- }
2847
- const modeHash = this.modeHasher(mode);
2848
- logger.debug(`[MessageQueue2] pushIsolateAndClear() called with mode hash: ${modeHash} - clearing ${this.queue.length} pending messages`);
2849
- this.queue = [];
2850
- this.queue.push({
2851
- message,
2852
- mode,
2853
- modeHash,
2854
- isolate: true
2855
- });
2856
- if (this.onMessageHandler) {
2857
- this.onMessageHandler(message, mode);
2858
- }
2859
- if (this.waiter) {
2860
- logger.debug(`[MessageQueue2] Notifying waiter for isolated message`);
2861
- const waiter = this.waiter;
2862
- this.waiter = null;
2863
- waiter(true);
2864
- }
2865
- logger.debug(`[MessageQueue2] pushIsolateAndClear() completed. Queue size: ${this.queue.length}`);
2866
- }
2867
- /**
2868
- * Push a message to the beginning of the queue with a mode.
2869
- */
2870
- unshift(message, mode) {
2871
- if (this.closed) {
2872
- throw new Error("Cannot unshift to closed queue");
2873
- }
2874
- const modeHash = this.modeHasher(mode);
2875
- logger.debug(`[MessageQueue2] unshift() called with mode hash: ${modeHash}`);
2876
- this.queue.unshift({
2877
- message,
2878
- mode,
2879
- modeHash,
2880
- isolate: false
2881
- });
2882
- if (this.onMessageHandler) {
2883
- this.onMessageHandler(message, mode);
2884
- }
2885
- if (this.waiter) {
2886
- logger.debug(`[MessageQueue2] Notifying waiter`);
2887
- const waiter = this.waiter;
2888
- this.waiter = null;
2889
- waiter(true);
2028
+ }
2029
+ nextHappyOrg = resetHappyOrgRuntimeForTask(messageHappyOrg.taskContext, specialistHome);
2030
+ } else {
2031
+ nextHappyOrg.taskContext = { ...messageHappyOrg.taskContext };
2890
2032
  }
2891
- logger.debug(`[MessageQueue2] unshift() completed. Queue size: ${this.queue.length}`);
2892
2033
  }
2893
- /**
2894
- * Reset the queue - clears all messages and resets to empty state
2895
- */
2896
- reset() {
2897
- logger.debug(`[MessageQueue2] reset() called. Clearing ${this.queue.length} messages`);
2898
- this.queue = [];
2899
- this.closed = false;
2900
- this.waiter = null;
2901
- }
2902
- /**
2903
- * Close the queue - no more messages can be pushed
2904
- */
2905
- close() {
2906
- logger.debug(`[MessageQueue2] close() called`);
2907
- this.closed = true;
2908
- if (this.waiter) {
2909
- const waiter = this.waiter;
2910
- this.waiter = null;
2911
- waiter(false);
2912
- }
2034
+ const taskContext = nextHappyOrg.taskContext ?? null;
2035
+ const replyContext = resolveReplyContextFromMessage(opts.message, taskContext);
2036
+ if (replyContext) {
2037
+ nextHappyOrg.replyContext = replyContext;
2038
+ } else if (!taskContext || currentHappyOrg.taskContext?.taskId !== taskContext.taskId) {
2039
+ nextHappyOrg.replyContext = null;
2913
2040
  }
2914
- /**
2915
- * Check if the queue is closed
2916
- */
2917
- isClosed() {
2918
- return this.closed;
2041
+ if (!taskContext) {
2042
+ return {
2043
+ nextMetadata: metadata,
2044
+ queuedTurn: null,
2045
+ blocked: false
2046
+ };
2919
2047
  }
2920
- /**
2921
- * Get the current queue size
2922
- */
2923
- size() {
2924
- return this.queue.length;
2048
+ const control = messageHappyOrg?.control;
2049
+ if (control?.action === "terminate") {
2050
+ nextHappyOrg.runtime = {
2051
+ status: "terminated",
2052
+ reason: normalizeOptionalText(control.reason) ?? "terminated_by_supervisor",
2053
+ terminatedAt: now
2054
+ };
2055
+ nextHappyOrg.activeOwner = null;
2056
+ return {
2057
+ nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
2058
+ queuedTurn: null,
2059
+ blocked: true,
2060
+ statusMessage: buildTerminatedStatusMessage(taskContext.taskId)
2061
+ };
2925
2062
  }
2926
- /**
2927
- * Wait for messages and return all messages with the same mode as a single string
2928
- * Returns { message: string, mode: T } or null if aborted/closed
2929
- */
2930
- async waitForMessagesAndGetAsString(abortSignal) {
2931
- if (this.queue.length > 0) {
2932
- return this.collectBatch();
2933
- }
2934
- if (this.closed || abortSignal?.aborted) {
2935
- return null;
2063
+ const hasReopenInputs = Boolean(
2064
+ normalizeOptionalText(control?.newContext) || normalizeOptionalText(control?.newDecision) || normalizeOptionalText(control?.newResource)
2065
+ );
2066
+ if (nextHappyOrg.runtime?.status === "terminated") {
2067
+ if (control?.action !== "reopen") {
2068
+ return {
2069
+ nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
2070
+ queuedTurn: null,
2071
+ blocked: true,
2072
+ statusMessage: buildTerminatedStatusMessage(taskContext.taskId)
2073
+ };
2936
2074
  }
2937
- const hasMessages = await this.waitForMessages(abortSignal);
2938
- if (!hasMessages) {
2939
- return null;
2075
+ if (!hasReopenInputs) {
2076
+ return {
2077
+ nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
2078
+ queuedTurn: null,
2079
+ blocked: true,
2080
+ statusMessage: `Task ${taskContext.taskId} can only reopen with new context, a new decision, or a new resource.`
2081
+ };
2940
2082
  }
2941
- return this.collectBatch();
2942
2083
  }
2943
- /**
2944
- * Collect a batch of messages with the same mode, respecting isolation requirements
2945
- */
2946
- collectBatch() {
2947
- if (this.queue.length === 0) {
2948
- return null;
2949
- }
2950
- const firstItem = this.queue[0];
2951
- const sameModeMessages = [];
2952
- let mode = firstItem.mode;
2953
- let isolate = firstItem.isolate ?? false;
2954
- const targetModeHash = firstItem.modeHash;
2955
- if (firstItem.isolate) {
2956
- const item = this.queue.shift();
2957
- sameModeMessages.push(item.message);
2958
- logger.debug(`[MessageQueue2] Collected isolated message with mode hash: ${targetModeHash}`);
2959
- } else {
2960
- while (this.queue.length > 0 && this.queue[0].modeHash === targetModeHash && !this.queue[0].isolate) {
2961
- const item = this.queue.shift();
2962
- sameModeMessages.push(item.message);
2963
- }
2964
- logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
2965
- }
2966
- const combinedMessage = sameModeMessages.join("\n");
2084
+ const reopenContext = control?.action === "reopen" && hasReopenInputs ? {
2085
+ newContext: normalizeOptionalText(control.newContext),
2086
+ newDecision: normalizeOptionalText(control.newDecision),
2087
+ newResource: normalizeOptionalText(control.newResource)
2088
+ } : void 0;
2089
+ if (reopenContext) {
2090
+ nextHappyOrg.runtime = {
2091
+ status: "active",
2092
+ reason: null,
2093
+ reopenedAt: now
2094
+ };
2095
+ } else if (!nextHappyOrg.runtime || nextHappyOrg.runtime.status !== "active") {
2096
+ nextHappyOrg.runtime = {
2097
+ status: "active",
2098
+ reason: null
2099
+ };
2100
+ }
2101
+ if (!nextHappyOrg.activeOwner) {
2102
+ nextHappyOrg.activeOwner = {
2103
+ ownerAgentId: taskContext.memberAgentId,
2104
+ ownerRunId: createRunId(taskContext, now),
2105
+ claimedAt: now
2106
+ };
2107
+ } else if (nextHappyOrg.activeOwner.ownerAgentId !== taskContext.memberAgentId) {
2967
2108
  return {
2968
- message: combinedMessage,
2969
- mode,
2970
- hash: targetModeHash,
2971
- isolate
2109
+ nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
2110
+ queuedTurn: null,
2111
+ blocked: true,
2112
+ statusMessage: buildOwnerConflictStatusMessage(
2113
+ taskContext.taskId,
2114
+ nextHappyOrg.activeOwner.ownerAgentId
2115
+ )
2972
2116
  };
2973
2117
  }
2974
- /**
2975
- * Wait for messages to arrive
2976
- */
2977
- waitForMessages(abortSignal) {
2978
- return new Promise((resolve) => {
2979
- let abortHandler = null;
2980
- if (abortSignal) {
2981
- abortHandler = () => {
2982
- logger.debug("[MessageQueue2] Wait aborted");
2983
- if (this.waiter === waiterFunc) {
2984
- this.waiter = null;
2985
- }
2986
- resolve(false);
2987
- };
2988
- abortSignal.addEventListener("abort", abortHandler);
2989
- }
2990
- const waiterFunc = (hasMessages) => {
2991
- if (abortHandler && abortSignal) {
2992
- abortSignal.removeEventListener("abort", abortHandler);
2993
- }
2994
- resolve(hasMessages);
2118
+ return {
2119
+ nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
2120
+ queuedTurn: {
2121
+ context: taskContext,
2122
+ ...reopenContext ? { reopenContext } : {},
2123
+ replyContext: cloneHappyOrgReplyContext(nextHappyOrg.replyContext),
2124
+ specialistHome: cloneHappyOrgSpecialistHomeIdentity(nextHappyOrg.specialistHome)
2125
+ },
2126
+ blocked: false
2127
+ };
2128
+ }
2129
+ function finalizeHappyOrgTurn(opts) {
2130
+ const metadata = opts.metadata ?? null;
2131
+ const queuedTurn = opts.queuedTurn ?? null;
2132
+ const { cleanedText, draft } = extractTaggedTurnReport(opts.responseText);
2133
+ if (!metadata || !queuedTurn) {
2134
+ return {
2135
+ cleanedText,
2136
+ report: null,
2137
+ nextMetadata: metadata
2138
+ };
2139
+ }
2140
+ const now = opts.now?.() ?? Date.now();
2141
+ const normalizedDraft = normalizeTurnReportDraft(draft);
2142
+ const reportTurnStatus = resolveReportedTurnStatus(opts.turnStatus, normalizedDraft);
2143
+ const currentHappyOrg = normalizeHappyOrgMetadata(metadata);
2144
+ const resolvedReplyContext = cloneHappyOrgReplyContext(queuedTurn.replyContext) ?? cloneHappyOrgReplyContext(currentHappyOrg.replyContext);
2145
+ const resolvedSpecialistHome = cloneHappyOrgSpecialistHomeIdentity(queuedTurn.specialistHome) ?? cloneHappyOrgSpecialistHomeIdentity(currentHappyOrg.specialistHome);
2146
+ const acceptanceHandoffCandidate = resolveHappyOrgAcceptanceHandoffCandidate(cleanedText);
2147
+ const acceptanceHandoff = acceptanceHandoffCandidate.outcome === "valid" && acceptanceHandoffCandidate.handoff.taskId === queuedTurn.context.taskId && (!resolvedReplyContext || acceptanceHandoffCandidate.handoff.dispatchId === resolvedReplyContext.dispatchId) ? acceptanceHandoffCandidate.handoff : null;
2148
+ const report = {
2149
+ ...queuedTurn.context,
2150
+ turnStatus: reportTurnStatus,
2151
+ interventionType: reportTurnStatus === "task_complete" ? "review_needed" : normalizedDraft.interventionType ?? "none",
2152
+ summary: normalizedDraft.summary ?? buildFallbackSummary(cleanedText, reportTurnStatus),
2153
+ blockerCode: normalizedDraft.blockerCode ?? null,
2154
+ decisionNeeded: normalizedDraft.decisionNeeded ?? null,
2155
+ targetArtifact: normalizedDraft.targetArtifact ?? null,
2156
+ accessChannelState: normalizedDraft.accessChannelState ?? "ok",
2157
+ repeatFingerprint: buildRepeatFingerprint(
2158
+ queuedTurn.context,
2159
+ normalizedDraft.blockerCode ?? null,
2160
+ normalizedDraft.targetArtifact ?? null
2161
+ ),
2162
+ replyContext: resolvedReplyContext,
2163
+ specialistHome: resolvedSpecialistHome,
2164
+ ...acceptanceHandoff ? { acceptanceHandoff } : {}
2165
+ };
2166
+ const nextHappyOrg = currentHappyOrg;
2167
+ nextHappyOrg.taskContext = { ...queuedTurn.context };
2168
+ nextHappyOrg.lastTurnReport = report;
2169
+ nextHappyOrg.activeOwner = null;
2170
+ nextHappyOrg.replyContext = resolvedReplyContext;
2171
+ nextHappyOrg.specialistHome = resolvedSpecialistHome;
2172
+ nextHappyOrg.repeat = nextHappyOrg.repeat ?? {
2173
+ threshold: HAPPY_ORG_REPEAT_THRESHOLD,
2174
+ fingerprints: {}
2175
+ };
2176
+ let terminateMessage;
2177
+ if (report.repeatFingerprint) {
2178
+ const currentEntry = nextHappyOrg.repeat.fingerprints[report.repeatFingerprint] ?? {
2179
+ count: 0};
2180
+ const nextCount = currentEntry.count + 1;
2181
+ nextHappyOrg.repeat.fingerprints[report.repeatFingerprint] = {
2182
+ count: nextCount,
2183
+ lastSeenAt: now
2184
+ };
2185
+ if (nextCount >= nextHappyOrg.repeat.threshold) {
2186
+ nextHappyOrg.runtime = {
2187
+ status: "terminated",
2188
+ reason: `repeat_fingerprint:${report.repeatFingerprint}`,
2189
+ terminatedAt: now
2995
2190
  };
2996
- if (this.queue.length > 0) {
2997
- if (abortHandler && abortSignal) {
2998
- abortSignal.removeEventListener("abort", abortHandler);
2999
- }
3000
- resolve(true);
3001
- return;
3002
- }
3003
- if (this.closed || abortSignal?.aborted) {
3004
- if (abortHandler && abortSignal) {
3005
- abortSignal.removeEventListener("abort", abortHandler);
3006
- }
3007
- resolve(false);
3008
- return;
3009
- }
3010
- this.waiter = waiterFunc;
3011
- logger.debug("[MessageQueue2] Waiting for messages...");
3012
- });
2191
+ terminateMessage = `Task ${queuedTurn.context.taskId} hit repeat threshold for ${report.repeatFingerprint} and is now terminated until reopen.`;
2192
+ } else if (!nextHappyOrg.runtime || nextHappyOrg.runtime.status !== "terminated") {
2193
+ nextHappyOrg.runtime = buildRuntimeStateAfterTurn(report);
2194
+ }
2195
+ } else if (!nextHappyOrg.runtime || nextHappyOrg.runtime.status !== "terminated") {
2196
+ nextHappyOrg.runtime = buildRuntimeStateAfterTurn(report);
3013
2197
  }
2198
+ return {
2199
+ cleanedText,
2200
+ report,
2201
+ nextMetadata: withHappyOrgMetadata(metadata, nextHappyOrg),
2202
+ terminateMessage
2203
+ };
2204
+ }
2205
+ async function finalizeHappyOrgTurnWithBusinessAck(opts) {
2206
+ const finalizedTurn = finalizeHappyOrgTurn({
2207
+ metadata: opts.metadata,
2208
+ queuedTurn: opts.queuedTurn,
2209
+ responseText: opts.responseText,
2210
+ turnStatus: opts.turnStatus,
2211
+ now: opts.now
2212
+ });
2213
+ const dispatchAckResult = await maybeSubmitHappyOrgDispatchBusinessAck({
2214
+ metadata: finalizedTurn.nextMetadata,
2215
+ queuedTurn: opts.queuedTurn,
2216
+ responseText: finalizedTurn.cleanedText,
2217
+ now: opts.now,
2218
+ submitDispatchAck: opts.submitDispatchAck
2219
+ });
2220
+ return {
2221
+ ...finalizedTurn,
2222
+ nextMetadata: dispatchAckResult.nextMetadata,
2223
+ dispatchAck: dispatchAckResult.dispatchAck
2224
+ };
3014
2225
  }
3015
2226
 
3016
- function registerKillSessionHandler(rpcHandlerManager, killThisHappy) {
3017
- rpcHandlerManager.registerHandler("killSession", async () => {
3018
- logger.debug("Kill session request received");
3019
- void killThisHappy();
3020
- return {
3021
- success: true,
3022
- message: "Killing happy-cli process"
2227
+ function supportsAgentStateUpdateEvents(sessionClient) {
2228
+ return typeof sessionClient.once === "function" && typeof sessionClient.off === "function";
2229
+ }
2230
+ async function syncControlledByUserState(sessionClient, controlledByUser) {
2231
+ if (!supportsAgentStateUpdateEvents(sessionClient)) {
2232
+ sessionClient.updateAgentState((currentState) => ({
2233
+ ...currentState,
2234
+ controlledByUser
2235
+ }));
2236
+ return;
2237
+ }
2238
+ await new Promise((resolve) => {
2239
+ let settled = false;
2240
+ const handleUpdated = () => {
2241
+ if (settled) {
2242
+ return;
2243
+ }
2244
+ settled = true;
2245
+ clearTimeout(timeout);
2246
+ sessionClient.off("agent-state-updated", handleUpdated);
2247
+ resolve();
3023
2248
  };
2249
+ const timeout = setTimeout(() => {
2250
+ if (settled) {
2251
+ return;
2252
+ }
2253
+ settled = true;
2254
+ sessionClient.off("agent-state-updated", handleUpdated);
2255
+ resolve();
2256
+ }, 1500);
2257
+ sessionClient.once("agent-state-updated", handleUpdated);
2258
+ sessionClient.updateAgentState((currentState) => ({
2259
+ ...currentState,
2260
+ controlledByUser
2261
+ }));
3024
2262
  });
3025
2263
  }
3026
2264
 
3027
- export { BasePermissionHandler as B, ConversationHistory$1 as C, INTERACTION_SUPERSEDED_ERROR as I, MissingMachineIdError as M, INTERACTION_TIMED_OUT_ERROR as a, MessageQueue2 as b, registerKillSessionHandler as c, MessageBuffer as d, ensureManagedProviderMachine as e, buildHappyOrgTurnPrompt as f, getPendingInteractionTimeoutMs as g, finalizeHappyOrgTurnWithBusinessAck as h, buildTurnResultPushNotification as i, buildReadyPushNotification as j, extractPermissionRequestPushContext as k, launchRuntimeHandleWithFactoryResult as l, buildPermissionPushNotification as m, renderTerminalOutputPreview as n, inferToolResultError as o, prepareTerminalOutputForForwarding as p, forwardAgentMessageToProviderSession as q, resolveHappyOrgQueuedTurn as r, syncControlledByUserState as s, createSessionTranscriptInkRenderer as t, waitForResponseCompleteWithAbort as w };
2265
+ export { BasePermissionHandler as B, INTERACTION_SUPERSEDED_ERROR as I, MissingMachineIdError as M, INTERACTION_TIMED_OUT_ERROR as a, MessageQueue2 as b, resolveHappyOrgQueuedTurn as c, buildHappyOrgTurnPrompt as d, ensureManagedProviderMachine as e, finalizeHappyOrgTurnWithBusinessAck as f, getPendingInteractionTimeoutMs as g, forwardAgentMessageToProviderSession as h, renderTerminalOutputPreview as i, inferToolResultError as j, prepareTerminalOutputForForwarding as p, registerKillSessionHandler as r, syncControlledByUserState as s, waitForResponseCompleteWithAbort as w };