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