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