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