@useorgx/openclaw-plugin 0.4.4 → 0.4.6
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/README.md +85 -2
- package/dashboard/dist/assets/0tOC3wSN.js +214 -0
- package/dashboard/dist/assets/Bm8QnMJ_.js +1 -0
- package/dashboard/dist/assets/CpJsfbXo.js +9 -0
- package/dashboard/dist/assets/CyxZio4Y.js +1 -0
- package/dashboard/dist/assets/DaAIOik3.css +1 -0
- package/dashboard/dist/index.html +3 -3
- package/dist/activity-store.d.ts +28 -0
- package/dist/activity-store.js +250 -0
- package/dist/agent-context-store.d.ts +19 -0
- package/dist/agent-context-store.js +60 -3
- package/dist/agent-suite.d.ts +83 -0
- package/dist/agent-suite.js +615 -0
- package/dist/contracts/client.d.ts +22 -1
- package/dist/contracts/client.js +120 -3
- package/dist/contracts/types.d.ts +190 -1
- package/dist/entity-comment-store.d.ts +29 -0
- package/dist/entity-comment-store.js +190 -0
- package/dist/hooks/post-reporting-event.mjs +326 -0
- package/dist/http-handler.d.ts +7 -1
- package/dist/http-handler.js +3619 -585
- package/dist/index.js +1039 -80
- package/dist/mcp-client-setup.d.ts +30 -0
- package/dist/mcp-client-setup.js +347 -0
- package/dist/mcp-http-handler.d.ts +55 -0
- package/dist/mcp-http-handler.js +395 -0
- package/dist/next-up-queue-store.d.ts +31 -0
- package/dist/next-up-queue-store.js +169 -0
- package/dist/openclaw.plugin.json +1 -1
- package/dist/outbox.d.ts +1 -1
- package/dist/runtime-instance-store.d.ts +1 -1
- package/dist/runtime-instance-store.js +20 -3
- package/dist/skill-pack-state.d.ts +69 -0
- package/dist/skill-pack-state.js +232 -0
- package/dist/worker-supervisor.d.ts +25 -0
- package/dist/worker-supervisor.js +62 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +10 -1
- package/skills/orgx-design-agent/SKILL.md +38 -0
- package/skills/orgx-engineering-agent/SKILL.md +55 -0
- package/skills/orgx-marketing-agent/SKILL.md +40 -0
- package/skills/orgx-operations-agent/SKILL.md +40 -0
- package/skills/orgx-orchestrator-agent/SKILL.md +45 -0
- package/skills/orgx-product-agent/SKILL.md +39 -0
- package/skills/orgx-sales-agent/SKILL.md +40 -0
- package/skills/ship/SKILL.md +63 -0
- package/dashboard/dist/assets/4hvaB0UC.js +0 -9
- package/dashboard/dist/assets/BgsfM2lz.js +0 -1
- package/dashboard/dist/assets/DCBlK4MX.js +0 -212
- package/dashboard/dist/assets/DEuY_RBN.js +0 -1
- package/dashboard/dist/assets/jyFhCND-.css +0 -1
package/dist/index.js
CHANGED
|
@@ -12,17 +12,25 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { OrgXClient } from "./api.js";
|
|
14
14
|
import { createHttpHandler } from "./http-handler.js";
|
|
15
|
-
import {
|
|
15
|
+
import { applyOrgxAgentSuitePlan, computeOrgxAgentSuitePlan } from "./agent-suite.js";
|
|
16
|
+
import { appendActivityItems } from "./activity-store.js";
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
18
|
import { join } from "node:path";
|
|
17
19
|
import { homedir } from "node:os";
|
|
18
20
|
import { fileURLToPath } from "node:url";
|
|
19
|
-
import { randomUUID } from "node:crypto";
|
|
21
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
20
22
|
import { clearPersistedApiKey, loadAuthStore, resolveInstallationId, saveAuthStore, } from "./auth-store.js";
|
|
21
23
|
import { clearPersistedSnapshot, readPersistedSnapshot, writePersistedSnapshot, } from "./snapshot-store.js";
|
|
22
24
|
import { appendToOutbox, readOutbox, readOutboxSummary, replaceOutbox, } from "./outbox.js";
|
|
25
|
+
import { getAgentContext, readAgentContexts } from "./agent-context-store.js";
|
|
26
|
+
import { readAgentRuns, markAgentRunStopped } from "./agent-run-store.js";
|
|
23
27
|
import { extractProgressOutboxMessage } from "./reporting/outbox-replay.js";
|
|
24
28
|
import { ensureGatewayWatchdog } from "./gateway-watchdog.js";
|
|
29
|
+
import { createMcpHttpHandler, } from "./mcp-http-handler.js";
|
|
30
|
+
import { autoConfigureDetectedMcpClients } from "./mcp-client-setup.js";
|
|
31
|
+
import { readOpenClawGatewayPort, readOpenClawSettingsSnapshot } from "./openclaw-settings.js";
|
|
25
32
|
import { posthogCapture } from "./telemetry/posthog.js";
|
|
33
|
+
import { readSkillPackState, refreshSkillPackState } from "./skill-pack-state.js";
|
|
26
34
|
export { OrgXClient } from "./api.js";
|
|
27
35
|
const DEFAULT_BASE_URL = "https://www.useorgx.com";
|
|
28
36
|
const DEFAULT_DOCS_URL = "https://orgx.mintlify.site/guides/openclaw-plugin-setup";
|
|
@@ -183,6 +191,7 @@ function resolveConfig(api, input) {
|
|
|
183
191
|
baseUrl,
|
|
184
192
|
syncIntervalMs: pluginConf.syncIntervalMs ?? 300_000,
|
|
185
193
|
enabled: pluginConf.enabled ?? true,
|
|
194
|
+
autoInstallAgentSuiteOnConnect: pluginConf.autoInstallAgentSuiteOnConnect ?? true,
|
|
186
195
|
dashboardEnabled: pluginConf.dashboardEnabled ?? true,
|
|
187
196
|
installationId: input.installationId,
|
|
188
197
|
pluginVersion: resolvePluginVersion(),
|
|
@@ -265,6 +274,29 @@ function isUuid(value) {
|
|
|
265
274
|
return false;
|
|
266
275
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
267
276
|
}
|
|
277
|
+
function inferReportingInitiativeId(input) {
|
|
278
|
+
const env = pickNonEmptyString(process.env.ORGX_INITIATIVE_ID);
|
|
279
|
+
if (isUuid(env))
|
|
280
|
+
return env;
|
|
281
|
+
const agentId = pickNonEmptyString(input.agent_id, input.agentId);
|
|
282
|
+
if (agentId) {
|
|
283
|
+
const ctx = getAgentContext(agentId);
|
|
284
|
+
const ctxInit = ctx?.initiativeId ?? undefined;
|
|
285
|
+
if (isUuid(ctxInit ?? undefined))
|
|
286
|
+
return ctxInit ?? undefined;
|
|
287
|
+
}
|
|
288
|
+
// Fall back to the most recently updated agent context with a UUID initiative id.
|
|
289
|
+
try {
|
|
290
|
+
const store = readAgentContexts();
|
|
291
|
+
const candidates = Object.values(store.agents ?? {}).filter((ctx) => isUuid(ctx?.initiativeId ?? undefined));
|
|
292
|
+
candidates.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
|
|
293
|
+
const picked = candidates[0]?.initiativeId ?? undefined;
|
|
294
|
+
return isUuid(picked) ? picked : undefined;
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
268
300
|
function toReportingPhase(phase, progressPct) {
|
|
269
301
|
if (progressPct === 100)
|
|
270
302
|
return "completed";
|
|
@@ -360,18 +392,73 @@ export default function register(api) {
|
|
|
360
392
|
pollIntervalMs: null,
|
|
361
393
|
};
|
|
362
394
|
let activePairing = null;
|
|
363
|
-
|
|
395
|
+
// NOTE: base URL can be updated at runtime (e.g. user edits OpenClaw config). Keep it mutable.
|
|
396
|
+
let baseApiUrl = config.baseUrl.replace(/\/+$/, "");
|
|
364
397
|
const defaultReportingCorrelationId = pickNonEmptyString(process.env.ORGX_CORRELATION_ID) ??
|
|
365
398
|
`openclaw-${config.installationId}`;
|
|
399
|
+
function refreshConfigFromSources(input) {
|
|
400
|
+
const allowApiKeyChanges = input?.allowApiKeyChanges !== false;
|
|
401
|
+
const previousApiKey = config.apiKey;
|
|
402
|
+
const previousBaseUrl = config.baseUrl;
|
|
403
|
+
const previousUserId = config.userId;
|
|
404
|
+
const previousDocsUrl = config.docsUrl;
|
|
405
|
+
const previousKeySource = config.apiKeySource;
|
|
406
|
+
const latestPersisted = loadAuthStore();
|
|
407
|
+
const next = resolveConfig(api, {
|
|
408
|
+
installationId: config.installationId,
|
|
409
|
+
persistedApiKey: latestPersisted?.apiKey ?? null,
|
|
410
|
+
persistedUserId: latestPersisted?.userId ?? null,
|
|
411
|
+
});
|
|
412
|
+
const nextApiKey = allowApiKeyChanges ? next.apiKey : previousApiKey;
|
|
413
|
+
const nextUserId = allowApiKeyChanges ? next.userId : previousUserId;
|
|
414
|
+
const changed = nextApiKey !== previousApiKey ||
|
|
415
|
+
next.baseUrl !== previousBaseUrl ||
|
|
416
|
+
nextUserId !== previousUserId ||
|
|
417
|
+
next.docsUrl !== previousDocsUrl ||
|
|
418
|
+
next.apiKeySource !== previousKeySource;
|
|
419
|
+
if (!changed) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
if (allowApiKeyChanges) {
|
|
423
|
+
config.apiKey = nextApiKey;
|
|
424
|
+
config.userId = nextUserId;
|
|
425
|
+
config.apiKeySource = next.apiKeySource;
|
|
426
|
+
}
|
|
427
|
+
config.baseUrl = next.baseUrl;
|
|
428
|
+
config.docsUrl = next.docsUrl;
|
|
429
|
+
baseApiUrl = config.baseUrl.replace(/\/+$/, "");
|
|
430
|
+
client.setCredentials({
|
|
431
|
+
apiKey: config.apiKey,
|
|
432
|
+
userId: config.userId,
|
|
433
|
+
baseUrl: config.baseUrl,
|
|
434
|
+
});
|
|
435
|
+
// Keep onboarding state aligned with what's actually configured (without forcing a status transition).
|
|
436
|
+
updateOnboardingState({
|
|
437
|
+
hasApiKey: Boolean(config.apiKey),
|
|
438
|
+
keySource: config.apiKeySource,
|
|
439
|
+
docsUrl: config.docsUrl,
|
|
440
|
+
installationId: config.installationId,
|
|
441
|
+
});
|
|
442
|
+
api.log?.info?.("[orgx] Config refreshed", {
|
|
443
|
+
reason: input?.reason ?? "runtime_refresh",
|
|
444
|
+
baseUrl: config.baseUrl,
|
|
445
|
+
hasApiKey: Boolean(config.apiKey),
|
|
446
|
+
apiKeySource: config.apiKeySource,
|
|
447
|
+
});
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
366
450
|
function resolveReportingContext(input) {
|
|
367
|
-
|
|
451
|
+
let initiativeId = pickNonEmptyString(input.initiative_id, input.initiativeId, process.env.ORGX_INITIATIVE_ID);
|
|
452
|
+
if (!isUuid(initiativeId)) {
|
|
453
|
+
initiativeId = inferReportingInitiativeId(input);
|
|
454
|
+
}
|
|
368
455
|
if (!initiativeId || !isUuid(initiativeId)) {
|
|
369
456
|
return {
|
|
370
457
|
ok: false,
|
|
371
458
|
error: "initiative_id is required (set ORGX_INITIATIVE_ID or pass initiative_id).",
|
|
372
459
|
};
|
|
373
460
|
}
|
|
374
|
-
const sourceCandidate = pickNonEmptyString(input.source_client, process.env.ORGX_SOURCE_CLIENT, "openclaw");
|
|
461
|
+
const sourceCandidate = pickNonEmptyString(input.source_client, input.sourceClient, process.env.ORGX_SOURCE_CLIENT, "openclaw");
|
|
375
462
|
const sourceClient = sourceCandidate === "codex" ||
|
|
376
463
|
sourceCandidate === "claude-code" ||
|
|
377
464
|
sourceCandidate === "api" ||
|
|
@@ -382,7 +469,10 @@ export default function register(api) {
|
|
|
382
469
|
const runId = isUuid(runIdCandidate) ? runIdCandidate : undefined;
|
|
383
470
|
const correlationId = runId
|
|
384
471
|
? undefined
|
|
385
|
-
: pickNonEmptyString(input.correlation_id,
|
|
472
|
+
: pickNonEmptyString(input.correlation_id, input.correlationId,
|
|
473
|
+
// Legacy: some buffered payloads only stored a local `runId` which is
|
|
474
|
+
// better treated as a correlation key than a server-backed run_id.
|
|
475
|
+
input.runId, defaultReportingCorrelationId, `openclaw-${Date.now()}`);
|
|
386
476
|
return {
|
|
387
477
|
ok: true,
|
|
388
478
|
value: {
|
|
@@ -405,6 +495,16 @@ export default function register(api) {
|
|
|
405
495
|
return err.message;
|
|
406
496
|
return typeof err === "string" ? err : "Unexpected error";
|
|
407
497
|
}
|
|
498
|
+
function stableHash(value) {
|
|
499
|
+
return createHash("sha256").update(value).digest("hex");
|
|
500
|
+
}
|
|
501
|
+
function isAuthFailure(err) {
|
|
502
|
+
const message = toErrorMessage(err).toLowerCase();
|
|
503
|
+
return (message.includes("401") ||
|
|
504
|
+
message.includes("unauthorized") ||
|
|
505
|
+
message.includes("invalid_token") ||
|
|
506
|
+
message.includes("invalid api key"));
|
|
507
|
+
}
|
|
408
508
|
const registerTool = api.registerTool.bind(api);
|
|
409
509
|
api.registerTool = (tool, options) => {
|
|
410
510
|
const toolName = tool.name;
|
|
@@ -556,22 +656,47 @@ export default function register(api) {
|
|
|
556
656
|
}
|
|
557
657
|
function buildManualKeyConnectUrl() {
|
|
558
658
|
try {
|
|
559
|
-
|
|
659
|
+
// Deep-link into the Security section where API keys live.
|
|
660
|
+
return new URL("/settings#security", baseApiUrl).toString();
|
|
560
661
|
}
|
|
561
662
|
catch {
|
|
562
|
-
return "https://www.useorgx.com/settings";
|
|
663
|
+
return "https://www.useorgx.com/settings#security";
|
|
563
664
|
}
|
|
564
665
|
}
|
|
565
|
-
async function fetchOrgxJson(method, path, body) {
|
|
666
|
+
async function fetchOrgxJson(method, path, body, options) {
|
|
566
667
|
try {
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
668
|
+
const controller = new AbortController();
|
|
669
|
+
const timeoutMs = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs)
|
|
670
|
+
? Math.max(1_000, Math.floor(options.timeoutMs))
|
|
671
|
+
: 12_000;
|
|
672
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
673
|
+
let response;
|
|
674
|
+
let rawText = "";
|
|
675
|
+
try {
|
|
676
|
+
response = await fetch(`${baseApiUrl}${path}`, {
|
|
677
|
+
method,
|
|
678
|
+
signal: controller.signal,
|
|
679
|
+
headers: {
|
|
680
|
+
Accept: "application/json",
|
|
681
|
+
"Content-Type": "application/json",
|
|
682
|
+
},
|
|
683
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
684
|
+
});
|
|
685
|
+
rawText = await response.text().catch(() => "");
|
|
686
|
+
}
|
|
687
|
+
finally {
|
|
688
|
+
clearTimeout(timeout);
|
|
689
|
+
}
|
|
690
|
+
const payload = (() => {
|
|
691
|
+
if (!rawText)
|
|
692
|
+
return null;
|
|
693
|
+
try {
|
|
694
|
+
return JSON.parse(rawText);
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
})();
|
|
575
700
|
if (!response.ok) {
|
|
576
701
|
const rawError = payload?.error ?? payload?.message;
|
|
577
702
|
let errorMessage;
|
|
@@ -584,18 +709,62 @@ export default function register(api) {
|
|
|
584
709
|
typeof rawError.message === "string") {
|
|
585
710
|
errorMessage = rawError.message;
|
|
586
711
|
}
|
|
712
|
+
else if (rawText && rawText.trim().length > 0) {
|
|
713
|
+
// Avoid dumping HTML (Cloudflare / Next.js error pages) into UI; keep it short.
|
|
714
|
+
const sanitized = rawText
|
|
715
|
+
.replace(/\s+/g, " ")
|
|
716
|
+
.replace(/<[^>]+>/g, "")
|
|
717
|
+
.trim();
|
|
718
|
+
errorMessage = sanitized.length > 0 ? sanitized.slice(0, 180) : `OrgX request failed (${response.status})`;
|
|
719
|
+
}
|
|
587
720
|
else {
|
|
588
721
|
errorMessage = `OrgX request failed (${response.status})`;
|
|
589
722
|
}
|
|
590
|
-
|
|
723
|
+
const statusToken = `HTTP ${response.status}`;
|
|
724
|
+
if (response.status &&
|
|
725
|
+
!errorMessage.toLowerCase().includes(statusToken.toLowerCase()) &&
|
|
726
|
+
!errorMessage.includes(`(${response.status})`)) {
|
|
727
|
+
errorMessage = `${errorMessage} (HTTP ${response.status})`;
|
|
728
|
+
}
|
|
729
|
+
const debugParts = [];
|
|
730
|
+
const requestId = response.headers.get("x-request-id");
|
|
731
|
+
const vercelId = response.headers.get("x-vercel-id");
|
|
732
|
+
const cfRay = response.headers.get("cf-ray");
|
|
733
|
+
const clerkStatus = response.headers.get("x-clerk-auth-status");
|
|
734
|
+
const clerkReason = response.headers.get("x-clerk-auth-reason");
|
|
735
|
+
if (requestId)
|
|
736
|
+
debugParts.push(`req=${requestId}`);
|
|
737
|
+
if (vercelId && vercelId !== requestId)
|
|
738
|
+
debugParts.push(`vercel=${vercelId}`);
|
|
739
|
+
if (cfRay)
|
|
740
|
+
debugParts.push(`cf-ray=${cfRay}`);
|
|
741
|
+
if (clerkStatus)
|
|
742
|
+
debugParts.push(`clerk=${clerkStatus}`);
|
|
743
|
+
if (clerkReason)
|
|
744
|
+
debugParts.push(`clerk_reason=${clerkReason}`);
|
|
745
|
+
const debugSuffix = debugParts.length > 0 ? ` (${debugParts.join(", ")})` : "";
|
|
746
|
+
return {
|
|
747
|
+
ok: false,
|
|
748
|
+
status: response.status,
|
|
749
|
+
error: `${errorMessage}${debugSuffix}`,
|
|
750
|
+
};
|
|
591
751
|
}
|
|
592
752
|
if (payload?.data !== undefined) {
|
|
593
753
|
return { ok: true, data: payload.data };
|
|
594
754
|
}
|
|
595
|
-
|
|
755
|
+
if (payload !== null) {
|
|
756
|
+
return { ok: true, data: payload };
|
|
757
|
+
}
|
|
758
|
+
return { ok: true, data: rawText };
|
|
596
759
|
}
|
|
597
760
|
catch (err) {
|
|
598
|
-
|
|
761
|
+
const message = err &&
|
|
762
|
+
typeof err === "object" &&
|
|
763
|
+
"name" in err &&
|
|
764
|
+
err.name === "AbortError"
|
|
765
|
+
? `OrgX request timed out (method=${method}, path=${path})`
|
|
766
|
+
: toErrorMessage(err);
|
|
767
|
+
return { ok: false, status: 0, error: message };
|
|
599
768
|
}
|
|
600
769
|
}
|
|
601
770
|
function setRuntimeApiKey(input) {
|
|
@@ -622,6 +791,23 @@ export default function register(api) {
|
|
|
622
791
|
installationId: config.installationId,
|
|
623
792
|
workspaceName: input.workspaceName ?? onboardingState.workspaceName,
|
|
624
793
|
});
|
|
794
|
+
if (input.source === "browser_pairing" &&
|
|
795
|
+
process.env.ORGX_DISABLE_MCP_CLIENT_AUTOCONFIG !== "1") {
|
|
796
|
+
try {
|
|
797
|
+
const snapshot = readOpenClawSettingsSnapshot();
|
|
798
|
+
const port = readOpenClawGatewayPort(snapshot.raw);
|
|
799
|
+
const localMcpUrl = `http://127.0.0.1:${port}/orgx/mcp`;
|
|
800
|
+
void autoConfigureDetectedMcpClients({
|
|
801
|
+
localMcpUrl,
|
|
802
|
+
logger: api.log ?? {},
|
|
803
|
+
}).catch(() => {
|
|
804
|
+
// best effort
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
catch {
|
|
808
|
+
// best effort
|
|
809
|
+
}
|
|
810
|
+
}
|
|
625
811
|
}
|
|
626
812
|
// ---------------------------------------------------------------------------
|
|
627
813
|
// 1. Background Sync Service
|
|
@@ -629,7 +815,6 @@ export default function register(api) {
|
|
|
629
815
|
let syncTimer = null;
|
|
630
816
|
let syncInFlight = null;
|
|
631
817
|
let syncServiceRunning = false;
|
|
632
|
-
const outboxQueues = ["progress", "decisions", "artifacts"];
|
|
633
818
|
let outboxReplayState = {
|
|
634
819
|
status: "idle",
|
|
635
820
|
lastReplayAttemptAt: null,
|
|
@@ -642,6 +827,7 @@ export default function register(api) {
|
|
|
642
827
|
const probeRemote = input.probeRemote === true;
|
|
643
828
|
const outbox = await readOutboxSummary();
|
|
644
829
|
const checks = [];
|
|
830
|
+
refreshConfigFromSources({ reason: "health_check" });
|
|
645
831
|
const hasApiKey = Boolean(config.apiKey);
|
|
646
832
|
if (hasApiKey) {
|
|
647
833
|
checks.push({
|
|
@@ -699,7 +885,9 @@ export default function register(api) {
|
|
|
699
885
|
else {
|
|
700
886
|
const startedAt = Date.now();
|
|
701
887
|
try {
|
|
702
|
-
|
|
888
|
+
// Avoid probing with /api/client/sync: it's heavier than necessary and can
|
|
889
|
+
// create false negatives during transient server slowness.
|
|
890
|
+
await client.getBillingStatus();
|
|
703
891
|
remoteReachable = true;
|
|
704
892
|
remoteLatencyMs = Date.now() - startedAt;
|
|
705
893
|
checks.push({
|
|
@@ -794,8 +982,305 @@ export default function register(api) {
|
|
|
794
982
|
.filter(Boolean);
|
|
795
983
|
return strings.length > 0 ? strings : undefined;
|
|
796
984
|
}
|
|
985
|
+
function isPidAlive(pid) {
|
|
986
|
+
if (!Number.isFinite(pid) || !pid || pid <= 0)
|
|
987
|
+
return false;
|
|
988
|
+
try {
|
|
989
|
+
process.kill(pid, 0);
|
|
990
|
+
return true;
|
|
991
|
+
}
|
|
992
|
+
catch {
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
function toFiniteNumber(value) {
|
|
997
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
998
|
+
return value;
|
|
999
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1000
|
+
const parsed = Number(value);
|
|
1001
|
+
if (Number.isFinite(parsed))
|
|
1002
|
+
return parsed;
|
|
1003
|
+
}
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
function isSafePathSegment(value) {
|
|
1007
|
+
const normalized = value.trim();
|
|
1008
|
+
if (!normalized || normalized === "." || normalized === "..")
|
|
1009
|
+
return false;
|
|
1010
|
+
if (normalized.includes("/") || normalized.includes("\\") || normalized.includes("\0")) {
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
if (normalized.includes(".."))
|
|
1014
|
+
return false;
|
|
1015
|
+
return true;
|
|
1016
|
+
}
|
|
1017
|
+
function parseRetroEntityType(value) {
|
|
1018
|
+
if (!value)
|
|
1019
|
+
return undefined;
|
|
1020
|
+
switch (value) {
|
|
1021
|
+
case "initiative":
|
|
1022
|
+
case "workstream":
|
|
1023
|
+
case "milestone":
|
|
1024
|
+
case "task":
|
|
1025
|
+
return value;
|
|
1026
|
+
default:
|
|
1027
|
+
return undefined;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
function readOpenClawSessionSummary(input) {
|
|
1031
|
+
const agentId = input.agentId.trim();
|
|
1032
|
+
const sessionId = input.sessionId.trim();
|
|
1033
|
+
if (!agentId || !sessionId) {
|
|
1034
|
+
return { tokens: 0, costUsd: 0, hadError: false, errorMessage: null };
|
|
1035
|
+
}
|
|
1036
|
+
if (!isSafePathSegment(agentId) || !isSafePathSegment(sessionId)) {
|
|
1037
|
+
return { tokens: 0, costUsd: 0, hadError: false, errorMessage: null };
|
|
1038
|
+
}
|
|
1039
|
+
const jsonlPath = join(homedir(), ".openclaw", "agents", agentId, "sessions", `${sessionId}.jsonl`);
|
|
1040
|
+
try {
|
|
1041
|
+
if (!existsSync(jsonlPath)) {
|
|
1042
|
+
return { tokens: 0, costUsd: 0, hadError: false, errorMessage: null };
|
|
1043
|
+
}
|
|
1044
|
+
const raw = readFileSync(jsonlPath, "utf8");
|
|
1045
|
+
const lines = raw.split("\n");
|
|
1046
|
+
let tokens = 0;
|
|
1047
|
+
let costUsd = 0;
|
|
1048
|
+
let hadError = false;
|
|
1049
|
+
let errorMessage = null;
|
|
1050
|
+
for (const line of lines) {
|
|
1051
|
+
const trimmed = line.trim();
|
|
1052
|
+
if (!trimmed)
|
|
1053
|
+
continue;
|
|
1054
|
+
try {
|
|
1055
|
+
const evt = JSON.parse(trimmed);
|
|
1056
|
+
if (evt.type !== "message")
|
|
1057
|
+
continue;
|
|
1058
|
+
const msg = evt.message;
|
|
1059
|
+
if (!msg || typeof msg !== "object")
|
|
1060
|
+
continue;
|
|
1061
|
+
const usage = msg.usage;
|
|
1062
|
+
if (usage && typeof usage === "object") {
|
|
1063
|
+
const totalTokens = toFiniteNumber(usage.totalTokens) ??
|
|
1064
|
+
toFiniteNumber(usage.total_tokens) ??
|
|
1065
|
+
null;
|
|
1066
|
+
const inputTokens = toFiniteNumber(usage.input) ?? 0;
|
|
1067
|
+
const outputTokens = toFiniteNumber(usage.output) ?? 0;
|
|
1068
|
+
const cacheReadTokens = toFiniteNumber(usage.cacheRead) ?? 0;
|
|
1069
|
+
const cacheWriteTokens = toFiniteNumber(usage.cacheWrite) ?? 0;
|
|
1070
|
+
tokens += Math.max(0, Math.round(totalTokens ??
|
|
1071
|
+
inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens));
|
|
1072
|
+
const cost = usage.cost;
|
|
1073
|
+
const costTotal = cost ? toFiniteNumber(cost.total) : null;
|
|
1074
|
+
if (costTotal !== null) {
|
|
1075
|
+
costUsd += Math.max(0, costTotal);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
const stopReason = typeof msg.stopReason === "string" ? msg.stopReason : "";
|
|
1079
|
+
const msgError = typeof msg.errorMessage === "string" && msg.errorMessage.trim().length > 0
|
|
1080
|
+
? msg.errorMessage.trim()
|
|
1081
|
+
: null;
|
|
1082
|
+
if (stopReason === "error" || msgError) {
|
|
1083
|
+
hadError = true;
|
|
1084
|
+
errorMessage = msgError ?? errorMessage;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
catch {
|
|
1088
|
+
// Ignore malformed lines.
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return {
|
|
1092
|
+
tokens,
|
|
1093
|
+
costUsd: Math.round(costUsd * 10_000) / 10_000,
|
|
1094
|
+
hadError,
|
|
1095
|
+
errorMessage,
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
catch {
|
|
1099
|
+
return { tokens: 0, costUsd: 0, hadError: false, errorMessage: null };
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
async function reconcileStoppedAgentRuns() {
|
|
1103
|
+
try {
|
|
1104
|
+
const store = readAgentRuns();
|
|
1105
|
+
const runs = Object.values(store.runs ?? {});
|
|
1106
|
+
for (const run of runs) {
|
|
1107
|
+
if (!run || typeof run !== "object")
|
|
1108
|
+
continue;
|
|
1109
|
+
if (run.status !== "running")
|
|
1110
|
+
continue;
|
|
1111
|
+
if (!run.pid || isPidAlive(run.pid))
|
|
1112
|
+
continue;
|
|
1113
|
+
const stopped = markAgentRunStopped(run.runId);
|
|
1114
|
+
if (!stopped)
|
|
1115
|
+
continue;
|
|
1116
|
+
const initiativeId = stopped.initiativeId?.trim() ?? "";
|
|
1117
|
+
if (!initiativeId)
|
|
1118
|
+
continue;
|
|
1119
|
+
const summary = readOpenClawSessionSummary({
|
|
1120
|
+
agentId: stopped.agentId,
|
|
1121
|
+
sessionId: stopped.runId,
|
|
1122
|
+
});
|
|
1123
|
+
const completedAt = stopped.stoppedAt ?? new Date().toISOString();
|
|
1124
|
+
const success = !summary.hadError;
|
|
1125
|
+
const correlationId = stopped.runId;
|
|
1126
|
+
const outcomePayload = {
|
|
1127
|
+
initiative_id: initiativeId,
|
|
1128
|
+
correlation_id: correlationId,
|
|
1129
|
+
source_client: "openclaw",
|
|
1130
|
+
execution_id: `openclaw:${stopped.runId}`,
|
|
1131
|
+
execution_type: "openclaw.session",
|
|
1132
|
+
agent_id: stopped.agentId,
|
|
1133
|
+
task_type: stopped.taskId ?? undefined,
|
|
1134
|
+
started_at: stopped.startedAt,
|
|
1135
|
+
completed_at: completedAt,
|
|
1136
|
+
inputs: {
|
|
1137
|
+
message: stopped.message,
|
|
1138
|
+
workstream_id: stopped.workstreamId,
|
|
1139
|
+
task_id: stopped.taskId,
|
|
1140
|
+
},
|
|
1141
|
+
outputs: {
|
|
1142
|
+
had_error: summary.hadError,
|
|
1143
|
+
error_message: summary.errorMessage,
|
|
1144
|
+
},
|
|
1145
|
+
steps: [],
|
|
1146
|
+
success,
|
|
1147
|
+
human_interventions: 0,
|
|
1148
|
+
errors: summary.errorMessage ? [summary.errorMessage] : [],
|
|
1149
|
+
metadata: {
|
|
1150
|
+
provider: stopped.provider,
|
|
1151
|
+
model: stopped.model,
|
|
1152
|
+
tokens: summary.tokens,
|
|
1153
|
+
cost_usd: summary.costUsd,
|
|
1154
|
+
source: "openclaw_agent_run_reconcile",
|
|
1155
|
+
},
|
|
1156
|
+
};
|
|
1157
|
+
const retroEntityType = stopped.taskId
|
|
1158
|
+
? "task"
|
|
1159
|
+
: "initiative";
|
|
1160
|
+
const retroEntityId = stopped.taskId ?? initiativeId;
|
|
1161
|
+
const retroSummary = stopped.taskId
|
|
1162
|
+
? `OpenClaw ${success ? "completed" : "blocked"} task ${stopped.taskId}.`
|
|
1163
|
+
: `OpenClaw run ${success ? "completed" : "blocked"} (session ${stopped.runId}).`;
|
|
1164
|
+
const retroPayload = {
|
|
1165
|
+
initiative_id: initiativeId,
|
|
1166
|
+
correlation_id: correlationId,
|
|
1167
|
+
source_client: "openclaw",
|
|
1168
|
+
entity_type: retroEntityType,
|
|
1169
|
+
entity_id: retroEntityId,
|
|
1170
|
+
title: stopped.taskId ?? stopped.runId,
|
|
1171
|
+
idempotency_key: `retro:${stopped.runId}`,
|
|
1172
|
+
retro: {
|
|
1173
|
+
summary: retroSummary,
|
|
1174
|
+
what_went_well: success ? ["Completed without runtime error."] : [],
|
|
1175
|
+
what_went_wrong: success
|
|
1176
|
+
? []
|
|
1177
|
+
: [summary.errorMessage ?? "Session ended with error."],
|
|
1178
|
+
decisions: [],
|
|
1179
|
+
follow_ups: success
|
|
1180
|
+
? []
|
|
1181
|
+
: [
|
|
1182
|
+
{
|
|
1183
|
+
title: "Investigate OpenClaw session failure and unblock task",
|
|
1184
|
+
priority: "p0",
|
|
1185
|
+
reason: summary.errorMessage ?? "Session ended with error.",
|
|
1186
|
+
},
|
|
1187
|
+
],
|
|
1188
|
+
signals: {
|
|
1189
|
+
tokens: summary.tokens,
|
|
1190
|
+
cost_usd: summary.costUsd,
|
|
1191
|
+
had_error: summary.hadError,
|
|
1192
|
+
error_message: summary.errorMessage,
|
|
1193
|
+
session_id: stopped.runId,
|
|
1194
|
+
task_id: stopped.taskId,
|
|
1195
|
+
workstream_id: stopped.workstreamId,
|
|
1196
|
+
provider: stopped.provider,
|
|
1197
|
+
model: stopped.model,
|
|
1198
|
+
source: "openclaw_agent_run_reconcile",
|
|
1199
|
+
},
|
|
1200
|
+
},
|
|
1201
|
+
};
|
|
1202
|
+
try {
|
|
1203
|
+
await client.recordRunOutcome(outcomePayload);
|
|
1204
|
+
}
|
|
1205
|
+
catch (err) {
|
|
1206
|
+
const timestamp = new Date().toISOString();
|
|
1207
|
+
const activityItem = {
|
|
1208
|
+
id: randomUUID(),
|
|
1209
|
+
type: "run_completed",
|
|
1210
|
+
title: `Buffered outcome for session ${stopped.runId}`,
|
|
1211
|
+
description: null,
|
|
1212
|
+
agentId: stopped.agentId,
|
|
1213
|
+
agentName: null,
|
|
1214
|
+
runId: stopped.runId,
|
|
1215
|
+
initiativeId,
|
|
1216
|
+
timestamp,
|
|
1217
|
+
phase: success ? "completed" : "blocked",
|
|
1218
|
+
summary: retroSummary,
|
|
1219
|
+
metadata: {
|
|
1220
|
+
source: "openclaw_local_fallback",
|
|
1221
|
+
error: toErrorMessage(err),
|
|
1222
|
+
},
|
|
1223
|
+
};
|
|
1224
|
+
await appendToOutbox(initiativeId, {
|
|
1225
|
+
id: randomUUID(),
|
|
1226
|
+
type: "outcome",
|
|
1227
|
+
timestamp,
|
|
1228
|
+
payload: outcomePayload,
|
|
1229
|
+
activityItem,
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
try {
|
|
1233
|
+
await client.recordRunRetro(retroPayload);
|
|
1234
|
+
}
|
|
1235
|
+
catch (err) {
|
|
1236
|
+
const timestamp = new Date().toISOString();
|
|
1237
|
+
const activityItem = {
|
|
1238
|
+
id: randomUUID(),
|
|
1239
|
+
type: "artifact_created",
|
|
1240
|
+
title: `Buffered retro for session ${stopped.runId}`,
|
|
1241
|
+
description: null,
|
|
1242
|
+
agentId: stopped.agentId,
|
|
1243
|
+
agentName: null,
|
|
1244
|
+
runId: stopped.runId,
|
|
1245
|
+
initiativeId,
|
|
1246
|
+
timestamp,
|
|
1247
|
+
phase: success ? "completed" : "blocked",
|
|
1248
|
+
summary: retroSummary,
|
|
1249
|
+
metadata: {
|
|
1250
|
+
source: "openclaw_local_fallback",
|
|
1251
|
+
error: toErrorMessage(err),
|
|
1252
|
+
},
|
|
1253
|
+
};
|
|
1254
|
+
await appendToOutbox(initiativeId, {
|
|
1255
|
+
id: randomUUID(),
|
|
1256
|
+
type: "retro",
|
|
1257
|
+
timestamp,
|
|
1258
|
+
payload: retroPayload,
|
|
1259
|
+
activityItem,
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
catch {
|
|
1265
|
+
// best effort
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
797
1268
|
async function replayOutboxEvent(event) {
|
|
798
1269
|
const payload = event.payload ?? {};
|
|
1270
|
+
function normalizeRunFields(context) {
|
|
1271
|
+
// We prefer correlation IDs for replay because many local adapters use UUID-like
|
|
1272
|
+
// session IDs that do *not* exist as server-side run IDs.
|
|
1273
|
+
if (context.correlationId) {
|
|
1274
|
+
return { run_id: undefined, correlation_id: context.correlationId };
|
|
1275
|
+
}
|
|
1276
|
+
if (context.runId) {
|
|
1277
|
+
return {
|
|
1278
|
+
run_id: undefined,
|
|
1279
|
+
correlation_id: `openclaw_run_${stableHash(context.runId).slice(0, 24)}`,
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
return { run_id: undefined, correlation_id: undefined };
|
|
1283
|
+
}
|
|
799
1284
|
if (event.type === "progress") {
|
|
800
1285
|
const message = extractProgressOutboxMessage(payload);
|
|
801
1286
|
if (!message) {
|
|
@@ -809,7 +1294,11 @@ export default function register(api) {
|
|
|
809
1294
|
throw new Error(context.error);
|
|
810
1295
|
}
|
|
811
1296
|
const rawPhase = pickStringField(payload, "phase") ?? "implementing";
|
|
812
|
-
const progressPct = typeof payload.progress_pct === "number"
|
|
1297
|
+
const progressPct = typeof payload.progress_pct === "number"
|
|
1298
|
+
? payload.progress_pct
|
|
1299
|
+
: typeof payload.progressPct === "number"
|
|
1300
|
+
? payload.progressPct
|
|
1301
|
+
: undefined;
|
|
813
1302
|
const phase = rawPhase === "intent" ||
|
|
814
1303
|
rawPhase === "execution" ||
|
|
815
1304
|
rawPhase === "blocked" ||
|
|
@@ -818,7 +1307,16 @@ export default function register(api) {
|
|
|
818
1307
|
rawPhase === "completed"
|
|
819
1308
|
? rawPhase
|
|
820
1309
|
: toReportingPhase(rawPhase, progressPct);
|
|
821
|
-
|
|
1310
|
+
const metaRaw = payload.metadata;
|
|
1311
|
+
const meta = metaRaw && typeof metaRaw === "object" && !Array.isArray(metaRaw)
|
|
1312
|
+
? metaRaw
|
|
1313
|
+
: {};
|
|
1314
|
+
const baseMetadata = {
|
|
1315
|
+
...meta,
|
|
1316
|
+
source: "orgx_openclaw_outbox_replay",
|
|
1317
|
+
outbox_event_id: event.id,
|
|
1318
|
+
};
|
|
1319
|
+
let emitPayload = {
|
|
822
1320
|
initiative_id: context.value.initiativeId,
|
|
823
1321
|
run_id: context.value.runId,
|
|
824
1322
|
correlation_id: context.value.correlationId,
|
|
@@ -827,12 +1325,53 @@ export default function register(api) {
|
|
|
827
1325
|
phase,
|
|
828
1326
|
progress_pct: progressPct,
|
|
829
1327
|
level: pickStringField(payload, "level"),
|
|
830
|
-
next_step: pickStringField(payload, "next_step") ??
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1328
|
+
next_step: pickStringField(payload, "next_step") ??
|
|
1329
|
+
pickStringField(payload, "nextStep") ??
|
|
1330
|
+
undefined,
|
|
1331
|
+
metadata: baseMetadata,
|
|
1332
|
+
};
|
|
1333
|
+
// Locally-buffered progress events often store a local UUID in run_id. OrgX may reject
|
|
1334
|
+
// unknown run IDs on replay; prefer a deterministic non-UUID correlation key instead.
|
|
1335
|
+
if (emitPayload.run_id && !emitPayload.correlation_id) {
|
|
1336
|
+
const replayCorrelationId = `openclaw_run_${stableHash(emitPayload.run_id).slice(0, 24)}`;
|
|
1337
|
+
emitPayload = {
|
|
1338
|
+
...emitPayload,
|
|
1339
|
+
run_id: undefined,
|
|
1340
|
+
correlation_id: replayCorrelationId,
|
|
1341
|
+
metadata: {
|
|
1342
|
+
...(emitPayload.metadata ?? {}),
|
|
1343
|
+
replay_run_id_as_correlation: true,
|
|
1344
|
+
},
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
try {
|
|
1348
|
+
await client.emitActivity(emitPayload);
|
|
1349
|
+
}
|
|
1350
|
+
catch (err) {
|
|
1351
|
+
// Some locally-buffered events carry a UUID that *looks* like an OrgX run_id
|
|
1352
|
+
// but was only ever used as a local correlation/grouping key. If OrgX
|
|
1353
|
+
// doesn't recognize it, retry by treating it as correlation_id so OrgX can
|
|
1354
|
+
// create/attach a run deterministically.
|
|
1355
|
+
const msg = toErrorMessage(err);
|
|
1356
|
+
if (emitPayload.run_id &&
|
|
1357
|
+
/^404\\b/.test(msg) &&
|
|
1358
|
+
/\\brun\\b/i.test(msg) &&
|
|
1359
|
+
/not found/i.test(msg)) {
|
|
1360
|
+
const replayCorrelationId = `openclaw_run_${stableHash(emitPayload.run_id).slice(0, 24)}`;
|
|
1361
|
+
await client.emitActivity({
|
|
1362
|
+
...emitPayload,
|
|
1363
|
+
run_id: undefined,
|
|
1364
|
+
correlation_id: replayCorrelationId,
|
|
1365
|
+
metadata: {
|
|
1366
|
+
...(emitPayload.metadata ?? {}),
|
|
1367
|
+
replay_run_id_as_correlation: true,
|
|
1368
|
+
},
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
else {
|
|
1372
|
+
throw err;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
836
1375
|
return;
|
|
837
1376
|
}
|
|
838
1377
|
if (event.type === "decision") {
|
|
@@ -847,12 +1386,29 @@ export default function register(api) {
|
|
|
847
1386
|
if (!context.ok) {
|
|
848
1387
|
throw new Error(context.error);
|
|
849
1388
|
}
|
|
1389
|
+
const runFields = normalizeRunFields({
|
|
1390
|
+
runId: context.value.runId,
|
|
1391
|
+
correlationId: context.value.correlationId,
|
|
1392
|
+
});
|
|
1393
|
+
// Payloads should include a stable idempotency_key when enqueued, but older
|
|
1394
|
+
// events may not. Derive a deterministic fallback so outbox replay won't
|
|
1395
|
+
// double-create the same remote decision.
|
|
1396
|
+
const fallbackKey = stableHash(JSON.stringify({
|
|
1397
|
+
t: "decision",
|
|
1398
|
+
initiative_id: context.value.initiativeId,
|
|
1399
|
+
run_id: context.value.runId ?? null,
|
|
1400
|
+
correlation_id: context.value.correlationId ?? null,
|
|
1401
|
+
question,
|
|
1402
|
+
})).slice(0, 24);
|
|
1403
|
+
const resolvedIdempotencyKey = pickStringField(payload, "idempotency_key") ??
|
|
1404
|
+
pickStringField(payload, "idempotencyKey") ??
|
|
1405
|
+
`openclaw:decision:${fallbackKey}`;
|
|
850
1406
|
await client.applyChangeset({
|
|
851
1407
|
initiative_id: context.value.initiativeId,
|
|
852
|
-
run_id:
|
|
853
|
-
correlation_id:
|
|
1408
|
+
run_id: runFields.run_id,
|
|
1409
|
+
correlation_id: runFields.correlation_id,
|
|
854
1410
|
source_client: context.value.sourceClient,
|
|
855
|
-
idempotency_key:
|
|
1411
|
+
idempotency_key: resolvedIdempotencyKey,
|
|
856
1412
|
operations: [
|
|
857
1413
|
{
|
|
858
1414
|
op: "decision.create",
|
|
@@ -871,6 +1427,10 @@ export default function register(api) {
|
|
|
871
1427
|
if (!context.ok) {
|
|
872
1428
|
throw new Error(context.error);
|
|
873
1429
|
}
|
|
1430
|
+
const runFields = normalizeRunFields({
|
|
1431
|
+
runId: context.value.runId,
|
|
1432
|
+
correlationId: context.value.correlationId,
|
|
1433
|
+
});
|
|
874
1434
|
const operations = Array.isArray(payload.operations)
|
|
875
1435
|
? payload.operations
|
|
876
1436
|
: [];
|
|
@@ -880,33 +1440,230 @@ export default function register(api) {
|
|
|
880
1440
|
});
|
|
881
1441
|
return;
|
|
882
1442
|
}
|
|
1443
|
+
// Status updates are the most common offline replay payload, and `updateEntity`
|
|
1444
|
+
// is the most widely supported primitive across OrgX deployments. Prefer it
|
|
1445
|
+
// when the changeset contains only simple status mutations.
|
|
1446
|
+
const statusOps = operations
|
|
1447
|
+
.map((op) => {
|
|
1448
|
+
if (!op || typeof op !== "object")
|
|
1449
|
+
return null;
|
|
1450
|
+
const record = op;
|
|
1451
|
+
const kind = typeof record.op === "string" ? record.op.trim() : "";
|
|
1452
|
+
if (kind === "task.update") {
|
|
1453
|
+
const taskId = typeof record.task_id === "string" ? record.task_id.trim() : "";
|
|
1454
|
+
const statusRaw = typeof record.status === "string" ? record.status.trim() : "";
|
|
1455
|
+
const normalized = statusRaw.toLowerCase().replace(/\s+/g, "_");
|
|
1456
|
+
const status = normalized === "completed" || normalized === "complete" || normalized === "finished"
|
|
1457
|
+
? "done"
|
|
1458
|
+
: normalized === "inprogress"
|
|
1459
|
+
? "in_progress"
|
|
1460
|
+
: normalized;
|
|
1461
|
+
if (!taskId || !status)
|
|
1462
|
+
return null;
|
|
1463
|
+
return { type: "task", id: taskId, status };
|
|
1464
|
+
}
|
|
1465
|
+
if (kind === "milestone.update") {
|
|
1466
|
+
const milestoneId = typeof record.milestone_id === "string" ? record.milestone_id.trim() : "";
|
|
1467
|
+
const statusRaw = typeof record.status === "string" ? record.status.trim() : "";
|
|
1468
|
+
const normalized = statusRaw.toLowerCase().replace(/\s+/g, "_");
|
|
1469
|
+
const status = normalized === "done" || normalized === "complete" || normalized === "finished"
|
|
1470
|
+
? "completed"
|
|
1471
|
+
: normalized === "inprogress"
|
|
1472
|
+
? "in_progress"
|
|
1473
|
+
: normalized === "todo" || normalized === "not_started" || normalized === "pending"
|
|
1474
|
+
? "planned"
|
|
1475
|
+
: normalized === "blocked" || normalized === "stuck"
|
|
1476
|
+
? "at_risk"
|
|
1477
|
+
: normalized;
|
|
1478
|
+
if (!milestoneId || !status)
|
|
1479
|
+
return null;
|
|
1480
|
+
return { type: "milestone", id: milestoneId, status };
|
|
1481
|
+
}
|
|
1482
|
+
return null;
|
|
1483
|
+
})
|
|
1484
|
+
.filter((item) => Boolean(item));
|
|
1485
|
+
if (statusOps.length === operations.length) {
|
|
1486
|
+
for (const op of statusOps) {
|
|
1487
|
+
await client.updateEntity(op.type, op.id, { status: op.status });
|
|
1488
|
+
}
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
// Payloads should include a stable idempotency_key when enqueued, but older
|
|
1492
|
+
// events may not. Derive a deterministic fallback so outbox replay won't
|
|
1493
|
+
// double-create the same remote change.
|
|
1494
|
+
const fallbackKey = stableHash(JSON.stringify({
|
|
1495
|
+
t: "changeset",
|
|
1496
|
+
initiative_id: context.value.initiativeId,
|
|
1497
|
+
run_id: context.value.runId ?? null,
|
|
1498
|
+
correlation_id: context.value.correlationId ?? null,
|
|
1499
|
+
operations,
|
|
1500
|
+
})).slice(0, 24);
|
|
1501
|
+
const resolvedIdempotencyKey = pickStringField(payload, "idempotency_key") ??
|
|
1502
|
+
pickStringField(payload, "idempotencyKey") ??
|
|
1503
|
+
`openclaw:changeset:${fallbackKey}`;
|
|
883
1504
|
await client.applyChangeset({
|
|
884
1505
|
initiative_id: context.value.initiativeId,
|
|
885
|
-
run_id:
|
|
886
|
-
correlation_id:
|
|
1506
|
+
run_id: runFields.run_id,
|
|
1507
|
+
correlation_id: runFields.correlation_id,
|
|
887
1508
|
source_client: context.value.sourceClient,
|
|
888
|
-
idempotency_key:
|
|
1509
|
+
idempotency_key: resolvedIdempotencyKey,
|
|
889
1510
|
operations,
|
|
890
1511
|
});
|
|
891
1512
|
return;
|
|
892
1513
|
}
|
|
893
|
-
if (event.type === "
|
|
894
|
-
const
|
|
895
|
-
if (!
|
|
896
|
-
|
|
1514
|
+
if (event.type === "outcome") {
|
|
1515
|
+
const context = resolveReportingContext(payload);
|
|
1516
|
+
if (!context.ok) {
|
|
1517
|
+
throw new Error(context.error);
|
|
1518
|
+
}
|
|
1519
|
+
const runFields = normalizeRunFields({
|
|
1520
|
+
runId: context.value.runId,
|
|
1521
|
+
correlationId: context.value.correlationId,
|
|
1522
|
+
});
|
|
1523
|
+
const executionId = pickStringField(payload, "execution_id") ??
|
|
1524
|
+
pickStringField(payload, "executionId");
|
|
1525
|
+
const executionType = pickStringField(payload, "execution_type") ??
|
|
1526
|
+
pickStringField(payload, "executionType");
|
|
1527
|
+
const agentId = pickStringField(payload, "agent_id") ??
|
|
1528
|
+
pickStringField(payload, "agentId");
|
|
1529
|
+
const success = typeof payload.success === "boolean"
|
|
1530
|
+
? payload.success
|
|
1531
|
+
: null;
|
|
1532
|
+
if (!executionId || !executionType || !agentId || success === null) {
|
|
1533
|
+
api.log?.warn?.("[orgx] Dropping invalid outcome outbox event", {
|
|
1534
|
+
eventId: event.id,
|
|
1535
|
+
});
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
const metaRaw = payload.metadata;
|
|
1539
|
+
const meta = metaRaw && typeof metaRaw === "object" && !Array.isArray(metaRaw)
|
|
1540
|
+
? metaRaw
|
|
1541
|
+
: {};
|
|
1542
|
+
await client.recordRunOutcome({
|
|
1543
|
+
initiative_id: context.value.initiativeId,
|
|
1544
|
+
run_id: runFields.run_id,
|
|
1545
|
+
correlation_id: runFields.correlation_id,
|
|
1546
|
+
source_client: context.value.sourceClient,
|
|
1547
|
+
execution_id: executionId,
|
|
1548
|
+
execution_type: executionType,
|
|
1549
|
+
agent_id: agentId,
|
|
1550
|
+
task_type: pickStringField(payload, "task_type") ??
|
|
1551
|
+
pickStringField(payload, "taskType") ??
|
|
1552
|
+
undefined,
|
|
1553
|
+
domain: pickStringField(payload, "domain") ?? undefined,
|
|
1554
|
+
started_at: pickStringField(payload, "started_at") ??
|
|
1555
|
+
pickStringField(payload, "startedAt") ??
|
|
1556
|
+
undefined,
|
|
1557
|
+
completed_at: pickStringField(payload, "completed_at") ??
|
|
1558
|
+
pickStringField(payload, "completedAt") ??
|
|
1559
|
+
undefined,
|
|
1560
|
+
inputs: payload.inputs && typeof payload.inputs === "object"
|
|
1561
|
+
? payload.inputs
|
|
1562
|
+
: undefined,
|
|
1563
|
+
outputs: payload.outputs && typeof payload.outputs === "object"
|
|
1564
|
+
? payload.outputs
|
|
1565
|
+
: undefined,
|
|
1566
|
+
steps: Array.isArray(payload.steps)
|
|
1567
|
+
? payload.steps
|
|
1568
|
+
: undefined,
|
|
1569
|
+
success,
|
|
1570
|
+
quality_score: typeof payload.quality_score === "number"
|
|
1571
|
+
? payload.quality_score
|
|
1572
|
+
: typeof payload.qualityScore === "number"
|
|
1573
|
+
? payload.qualityScore
|
|
1574
|
+
: undefined,
|
|
1575
|
+
duration_vs_estimate: typeof payload.duration_vs_estimate === "number"
|
|
1576
|
+
? payload.duration_vs_estimate
|
|
1577
|
+
: typeof payload.durationVsEstimate === "number"
|
|
1578
|
+
? payload.durationVsEstimate
|
|
1579
|
+
: undefined,
|
|
1580
|
+
cost_vs_budget: typeof payload.cost_vs_budget === "number"
|
|
1581
|
+
? payload.cost_vs_budget
|
|
1582
|
+
: typeof payload.costVsBudget === "number"
|
|
1583
|
+
? payload.costVsBudget
|
|
1584
|
+
: undefined,
|
|
1585
|
+
human_interventions: typeof payload.human_interventions === "number"
|
|
1586
|
+
? payload.human_interventions
|
|
1587
|
+
: typeof payload.humanInterventions === "number"
|
|
1588
|
+
? payload.humanInterventions
|
|
1589
|
+
: undefined,
|
|
1590
|
+
user_satisfaction: typeof payload.user_satisfaction === "number"
|
|
1591
|
+
? payload.user_satisfaction
|
|
1592
|
+
: typeof payload.userSatisfaction === "number"
|
|
1593
|
+
? payload.userSatisfaction
|
|
1594
|
+
: undefined,
|
|
1595
|
+
errors: Array.isArray(payload.errors)
|
|
1596
|
+
? payload.errors.filter((e) => typeof e === "string")
|
|
1597
|
+
: undefined,
|
|
1598
|
+
metadata: {
|
|
1599
|
+
...meta,
|
|
1600
|
+
source: "orgx_openclaw_outbox_replay",
|
|
1601
|
+
outbox_event_id: event.id,
|
|
1602
|
+
},
|
|
1603
|
+
});
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
if (event.type === "retro") {
|
|
1607
|
+
const context = resolveReportingContext(payload);
|
|
1608
|
+
if (!context.ok) {
|
|
1609
|
+
throw new Error(context.error);
|
|
1610
|
+
}
|
|
1611
|
+
const runFields = normalizeRunFields({
|
|
1612
|
+
runId: context.value.runId,
|
|
1613
|
+
correlationId: context.value.correlationId,
|
|
1614
|
+
});
|
|
1615
|
+
const retro = payload.retro && typeof payload.retro === "object" && !Array.isArray(payload.retro)
|
|
1616
|
+
? payload.retro
|
|
1617
|
+
: null;
|
|
1618
|
+
const summary = retro && typeof retro.summary === "string" ? retro.summary.trim() : "";
|
|
1619
|
+
if (!retro || !summary) {
|
|
1620
|
+
api.log?.warn?.("[orgx] Dropping invalid retro outbox event", {
|
|
897
1621
|
eventId: event.id,
|
|
898
1622
|
});
|
|
899
1623
|
return;
|
|
900
1624
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1625
|
+
const entityTypeRaw = pickStringField(payload, "entity_type") ??
|
|
1626
|
+
pickStringField(payload, "entityType");
|
|
1627
|
+
const parsedEntityType = parseRetroEntityType(entityTypeRaw) ?? null;
|
|
1628
|
+
// Server-side enum parity can lag behind local clients. Only attach to the
|
|
1629
|
+
// entity types that are guaranteed to exist today.
|
|
1630
|
+
const entityType = parsedEntityType === "initiative" || parsedEntityType === "task"
|
|
1631
|
+
? parsedEntityType
|
|
1632
|
+
: null;
|
|
1633
|
+
const entityIdRaw = pickStringField(payload, "entity_id") ??
|
|
1634
|
+
pickStringField(payload, "entityId") ??
|
|
1635
|
+
null;
|
|
1636
|
+
const entityId = isUuid(entityIdRaw ?? undefined) ? entityIdRaw : null;
|
|
1637
|
+
await client.recordRunRetro({
|
|
1638
|
+
initiative_id: context.value.initiativeId,
|
|
1639
|
+
run_id: runFields.run_id,
|
|
1640
|
+
correlation_id: runFields.correlation_id,
|
|
1641
|
+
source_client: context.value.sourceClient,
|
|
1642
|
+
entity_type: entityType && entityId ? entityType : undefined,
|
|
1643
|
+
entity_id: entityType && entityId ? entityId : undefined,
|
|
1644
|
+
title: pickStringField(payload, "title") ?? undefined,
|
|
1645
|
+
idempotency_key: pickStringField(payload, "idempotency_key") ??
|
|
1646
|
+
pickStringField(payload, "idempotencyKey") ??
|
|
1647
|
+
undefined,
|
|
1648
|
+
retro: retro,
|
|
1649
|
+
markdown: pickStringField(payload, "markdown") ?? undefined,
|
|
907
1650
|
});
|
|
908
1651
|
return;
|
|
909
1652
|
}
|
|
1653
|
+
if (event.type === "artifact") {
|
|
1654
|
+
// Artifacts are UI-level breadcrumbs and may not be supported by every
|
|
1655
|
+
// OrgX deployment's `/api/entities` schema. Persist locally and drop from
|
|
1656
|
+
// the outbox so progress reporting doesn't wedge on irreplayable items.
|
|
1657
|
+
try {
|
|
1658
|
+
if (event.activityItem) {
|
|
1659
|
+
appendActivityItems([event.activityItem]);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
catch {
|
|
1663
|
+
// best effort
|
|
1664
|
+
}
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
910
1667
|
}
|
|
911
1668
|
async function flushOutboxQueues() {
|
|
912
1669
|
const attemptAt = new Date().toISOString();
|
|
@@ -918,7 +1675,14 @@ export default function register(api) {
|
|
|
918
1675
|
};
|
|
919
1676
|
let hadReplayFailure = false;
|
|
920
1677
|
let lastReplayError = null;
|
|
921
|
-
|
|
1678
|
+
// Outbox files are keyed by *session id* (e.g. initiative/run correlation),
|
|
1679
|
+
// not by event type.
|
|
1680
|
+
const outboxSummary = await readOutboxSummary();
|
|
1681
|
+
const queues = Object.entries(outboxSummary.pendingByQueue)
|
|
1682
|
+
.filter(([, count]) => typeof count === "number" && count > 0)
|
|
1683
|
+
.map(([queueId]) => queueId)
|
|
1684
|
+
.sort();
|
|
1685
|
+
for (const queue of queues) {
|
|
922
1686
|
const pending = await readOutbox(queue);
|
|
923
1687
|
if (pending.length === 0) {
|
|
924
1688
|
continue;
|
|
@@ -971,6 +1735,9 @@ export default function register(api) {
|
|
|
971
1735
|
return syncInFlight;
|
|
972
1736
|
}
|
|
973
1737
|
syncInFlight = (async () => {
|
|
1738
|
+
if (!config.apiKey) {
|
|
1739
|
+
refreshConfigFromSources({ reason: "sync_no_api_key" });
|
|
1740
|
+
}
|
|
974
1741
|
if (!config.apiKey) {
|
|
975
1742
|
updateOnboardingState({
|
|
976
1743
|
status: "idle",
|
|
@@ -981,25 +1748,123 @@ export default function register(api) {
|
|
|
981
1748
|
return;
|
|
982
1749
|
}
|
|
983
1750
|
try {
|
|
984
|
-
|
|
1751
|
+
await reconcileStoppedAgentRuns();
|
|
1752
|
+
let snapshotError = null;
|
|
1753
|
+
try {
|
|
1754
|
+
updateCachedSnapshot(await client.getOrgSnapshot());
|
|
1755
|
+
}
|
|
1756
|
+
catch (err) {
|
|
1757
|
+
if (isAuthFailure(err)) {
|
|
1758
|
+
throw err;
|
|
1759
|
+
}
|
|
1760
|
+
snapshotError = toErrorMessage(err);
|
|
1761
|
+
api.log?.warn?.("[orgx] Snapshot sync failed (continuing)", {
|
|
1762
|
+
error: snapshotError,
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
// Best-effort: poll the canonical OrgX SkillPack so the dashboard/install path
|
|
1766
|
+
// can apply it without blocking on an on-demand fetch.
|
|
1767
|
+
try {
|
|
1768
|
+
const refreshed = await refreshSkillPackState({
|
|
1769
|
+
getSkillPack: (args) => client.getSkillPack(args),
|
|
1770
|
+
});
|
|
1771
|
+
if (refreshed.changed) {
|
|
1772
|
+
void posthogCapture({
|
|
1773
|
+
event: "openclaw_skill_pack_updated",
|
|
1774
|
+
distinctId: config.installationId,
|
|
1775
|
+
properties: {
|
|
1776
|
+
plugin_version: config.pluginVersion,
|
|
1777
|
+
skill_pack_name: refreshed.state.pack?.name ?? null,
|
|
1778
|
+
skill_pack_version: refreshed.state.pack?.version ?? null,
|
|
1779
|
+
skill_pack_checksum: refreshed.state.pack?.checksum ?? null,
|
|
1780
|
+
},
|
|
1781
|
+
}).catch(() => {
|
|
1782
|
+
// best effort
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
catch {
|
|
1787
|
+
// best effort
|
|
1788
|
+
}
|
|
1789
|
+
// Best-effort: provision/update the OrgX agent suite after we've verified a working connection.
|
|
1790
|
+
// This makes domain agents available immediately for launches without requiring a manual install.
|
|
1791
|
+
try {
|
|
1792
|
+
if (config.autoInstallAgentSuiteOnConnect !== false) {
|
|
1793
|
+
const state = readSkillPackState();
|
|
1794
|
+
const updateAvailable = Boolean(state.remote?.checksum &&
|
|
1795
|
+
state.pack?.checksum &&
|
|
1796
|
+
state.remote.checksum !== state.pack.checksum);
|
|
1797
|
+
const plan = computeOrgxAgentSuitePlan({
|
|
1798
|
+
packVersion: config.pluginVersion || "0.0.0",
|
|
1799
|
+
skillPack: state.overrides,
|
|
1800
|
+
skillPackRemote: state.remote,
|
|
1801
|
+
skillPackPolicy: state.policy,
|
|
1802
|
+
skillPackUpdateAvailable: updateAvailable,
|
|
1803
|
+
});
|
|
1804
|
+
const hasConflicts = (plan.workspaceFiles ?? []).some((f) => f.action === "conflict");
|
|
1805
|
+
const hasWork = Boolean(plan.openclawConfigWouldUpdate) ||
|
|
1806
|
+
(plan.workspaceFiles ?? []).some((f) => f.action !== "noop");
|
|
1807
|
+
if (hasWork && !hasConflicts) {
|
|
1808
|
+
const applied = applyOrgxAgentSuitePlan({
|
|
1809
|
+
plan,
|
|
1810
|
+
dryRun: false,
|
|
1811
|
+
skillPack: state.overrides,
|
|
1812
|
+
});
|
|
1813
|
+
void applied;
|
|
1814
|
+
void posthogCapture({
|
|
1815
|
+
event: "openclaw_agent_suite_auto_install",
|
|
1816
|
+
distinctId: config.installationId,
|
|
1817
|
+
properties: {
|
|
1818
|
+
plugin_version: (config.pluginVersion ?? "").trim() || null,
|
|
1819
|
+
skill_pack_source: plan.skillPack?.source ?? null,
|
|
1820
|
+
skill_pack_checksum: plan.skillPack?.checksum ?? null,
|
|
1821
|
+
skill_pack_version: plan.skillPack?.version ?? null,
|
|
1822
|
+
openclaw_config_updated: Boolean(plan.openclawConfigWouldUpdate),
|
|
1823
|
+
},
|
|
1824
|
+
}).catch(() => {
|
|
1825
|
+
// best effort
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
catch (err) {
|
|
1831
|
+
api.log?.debug?.("[orgx] Agent suite auto-provision skipped/failed (best effort)", {
|
|
1832
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
985
1835
|
updateOnboardingState({
|
|
986
1836
|
status: "connected",
|
|
987
1837
|
hasApiKey: true,
|
|
988
|
-
connectionVerified:
|
|
989
|
-
lastError:
|
|
1838
|
+
connectionVerified: snapshotError === null,
|
|
1839
|
+
lastError: snapshotError,
|
|
990
1840
|
nextAction: "open_dashboard",
|
|
991
1841
|
});
|
|
992
1842
|
await flushOutboxQueues();
|
|
993
1843
|
api.log?.debug?.("[orgx] Sync OK");
|
|
994
1844
|
}
|
|
995
1845
|
catch (err) {
|
|
1846
|
+
const authFailure = isAuthFailure(err);
|
|
1847
|
+
const errorMessage = authFailure
|
|
1848
|
+
? "Unauthorized. Your OrgX key may be revoked or expired. Reconnect in browser or use API key."
|
|
1849
|
+
: toErrorMessage(err);
|
|
996
1850
|
updateOnboardingState({
|
|
997
1851
|
status: "error",
|
|
998
1852
|
hasApiKey: true,
|
|
999
1853
|
connectionVerified: false,
|
|
1000
|
-
lastError:
|
|
1854
|
+
lastError: errorMessage,
|
|
1001
1855
|
nextAction: "reconnect",
|
|
1002
1856
|
});
|
|
1857
|
+
if (authFailure) {
|
|
1858
|
+
void posthogCapture({
|
|
1859
|
+
event: "openclaw_sync_auth_failed",
|
|
1860
|
+
distinctId: config.installationId,
|
|
1861
|
+
properties: {
|
|
1862
|
+
plugin_version: config.pluginVersion,
|
|
1863
|
+
},
|
|
1864
|
+
}).catch(() => {
|
|
1865
|
+
// best effort
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1003
1868
|
api.log?.warn?.(`[orgx] Sync failed: ${err instanceof Error ? err.message : err}`);
|
|
1004
1869
|
}
|
|
1005
1870
|
})();
|
|
@@ -1031,7 +1896,10 @@ export default function register(api) {
|
|
|
1031
1896
|
openclawVersion: input.openclawVersion,
|
|
1032
1897
|
platform: input.platform || process.platform,
|
|
1033
1898
|
deviceName: input.deviceName,
|
|
1034
|
-
}
|
|
1899
|
+
},
|
|
1900
|
+
// Pairing can hit a cold serverless boot + supabase insert + rate-limit checks.
|
|
1901
|
+
// Give it more headroom than typical lightweight API calls.
|
|
1902
|
+
{ timeoutMs: 30_000 });
|
|
1035
1903
|
if (!started.ok) {
|
|
1036
1904
|
if (isAuthRequiredError(started)) {
|
|
1037
1905
|
clearPairingState();
|
|
@@ -1055,7 +1923,8 @@ export default function register(api) {
|
|
|
1055
1923
|
state,
|
|
1056
1924
|
};
|
|
1057
1925
|
}
|
|
1058
|
-
const
|
|
1926
|
+
const statusLabel = started.status ? ` (HTTP ${started.status})` : "";
|
|
1927
|
+
const message = `Pairing start failed${statusLabel}: ${started.error}`;
|
|
1059
1928
|
updateOnboardingState({
|
|
1060
1929
|
status: "error",
|
|
1061
1930
|
hasApiKey: Boolean(config.apiKey),
|
|
@@ -1363,8 +2232,13 @@ export default function register(api) {
|
|
|
1363
2232
|
// ---------------------------------------------------------------------------
|
|
1364
2233
|
// 2. MCP Tools (Model Context Protocol compatible)
|
|
1365
2234
|
// ---------------------------------------------------------------------------
|
|
2235
|
+
const mcpToolRegistry = new Map();
|
|
2236
|
+
const registerMcpTool = (tool, options) => {
|
|
2237
|
+
mcpToolRegistry.set(tool.name, tool);
|
|
2238
|
+
api.registerTool(tool, options);
|
|
2239
|
+
};
|
|
1366
2240
|
// --- orgx_status ---
|
|
1367
|
-
|
|
2241
|
+
registerMcpTool({
|
|
1368
2242
|
name: "orgx_status",
|
|
1369
2243
|
description: "Get current OrgX org status: active initiatives, agent states, pending decisions, active tasks.",
|
|
1370
2244
|
parameters: {
|
|
@@ -1384,7 +2258,7 @@ export default function register(api) {
|
|
|
1384
2258
|
},
|
|
1385
2259
|
}, { optional: true });
|
|
1386
2260
|
// --- orgx_sync ---
|
|
1387
|
-
|
|
2261
|
+
registerMcpTool({
|
|
1388
2262
|
name: "orgx_sync",
|
|
1389
2263
|
description: "Push/pull memory sync with OrgX. Send local memory/daily log; receive initiatives, tasks, decisions, model routing policy.",
|
|
1390
2264
|
parameters: {
|
|
@@ -1414,7 +2288,7 @@ export default function register(api) {
|
|
|
1414
2288
|
},
|
|
1415
2289
|
}, { optional: true });
|
|
1416
2290
|
// --- orgx_delegation_preflight ---
|
|
1417
|
-
|
|
2291
|
+
registerMcpTool({
|
|
1418
2292
|
name: "orgx_delegation_preflight",
|
|
1419
2293
|
description: "Run delegation preflight to score scope quality, estimate ETA/cost, and suggest a split before autonomous execution.",
|
|
1420
2294
|
parameters: {
|
|
@@ -1465,7 +2339,7 @@ export default function register(api) {
|
|
|
1465
2339
|
},
|
|
1466
2340
|
}, { optional: true });
|
|
1467
2341
|
// --- orgx_run_action ---
|
|
1468
|
-
|
|
2342
|
+
registerMcpTool({
|
|
1469
2343
|
name: "orgx_run_action",
|
|
1470
2344
|
description: "Apply a control action to a run: pause, resume, cancel, or rollback (rollback requires checkpointId).",
|
|
1471
2345
|
parameters: {
|
|
@@ -1509,7 +2383,7 @@ export default function register(api) {
|
|
|
1509
2383
|
},
|
|
1510
2384
|
}, { optional: true });
|
|
1511
2385
|
// --- orgx_checkpoints_list ---
|
|
1512
|
-
|
|
2386
|
+
registerMcpTool({
|
|
1513
2387
|
name: "orgx_checkpoints_list",
|
|
1514
2388
|
description: "List checkpoints for a run.",
|
|
1515
2389
|
parameters: {
|
|
@@ -1534,7 +2408,7 @@ export default function register(api) {
|
|
|
1534
2408
|
},
|
|
1535
2409
|
}, { optional: true });
|
|
1536
2410
|
// --- orgx_checkpoint_restore ---
|
|
1537
|
-
|
|
2411
|
+
registerMcpTool({
|
|
1538
2412
|
name: "orgx_checkpoint_restore",
|
|
1539
2413
|
description: "Restore a run to a specific checkpoint.",
|
|
1540
2414
|
parameters: {
|
|
@@ -1573,7 +2447,7 @@ export default function register(api) {
|
|
|
1573
2447
|
},
|
|
1574
2448
|
}, { optional: true });
|
|
1575
2449
|
// --- orgx_spawn_check ---
|
|
1576
|
-
|
|
2450
|
+
registerMcpTool({
|
|
1577
2451
|
name: "orgx_spawn_check",
|
|
1578
2452
|
description: "Check quality gate + get model routing before spawning a sub-agent. Returns allowed/denied, model tier, and check details.",
|
|
1579
2453
|
parameters: {
|
|
@@ -1602,7 +2476,7 @@ export default function register(api) {
|
|
|
1602
2476
|
},
|
|
1603
2477
|
}, { optional: true });
|
|
1604
2478
|
// --- orgx_quality_score ---
|
|
1605
|
-
|
|
2479
|
+
registerMcpTool({
|
|
1606
2480
|
name: "orgx_quality_score",
|
|
1607
2481
|
description: "Record a quality score (1-5) for completed agent work. Used to gate future spawns and track performance.",
|
|
1608
2482
|
parameters: {
|
|
@@ -1640,7 +2514,7 @@ export default function register(api) {
|
|
|
1640
2514
|
},
|
|
1641
2515
|
}, { optional: true });
|
|
1642
2516
|
// --- orgx_create_entity ---
|
|
1643
|
-
|
|
2517
|
+
registerMcpTool({
|
|
1644
2518
|
name: "orgx_create_entity",
|
|
1645
2519
|
description: "Create an OrgX entity (initiative, workstream, task, decision, milestone, etc.).",
|
|
1646
2520
|
parameters: {
|
|
@@ -1725,7 +2599,7 @@ export default function register(api) {
|
|
|
1725
2599
|
},
|
|
1726
2600
|
}, { optional: true });
|
|
1727
2601
|
// --- orgx_update_entity ---
|
|
1728
|
-
|
|
2602
|
+
registerMcpTool({
|
|
1729
2603
|
name: "orgx_update_entity",
|
|
1730
2604
|
description: "Update an existing OrgX entity by type and ID.",
|
|
1731
2605
|
parameters: {
|
|
@@ -1766,7 +2640,7 @@ export default function register(api) {
|
|
|
1766
2640
|
},
|
|
1767
2641
|
}, { optional: true });
|
|
1768
2642
|
// --- orgx_list_entities ---
|
|
1769
|
-
|
|
2643
|
+
registerMcpTool({
|
|
1770
2644
|
name: "orgx_list_entities",
|
|
1771
2645
|
description: "List OrgX entities of a given type with optional status filter.",
|
|
1772
2646
|
parameters: {
|
|
@@ -1801,6 +2675,48 @@ export default function register(api) {
|
|
|
1801
2675
|
}
|
|
1802
2676
|
},
|
|
1803
2677
|
}, { optional: true });
|
|
2678
|
+
function withProvenanceMetadata(metadata) {
|
|
2679
|
+
const input = metadata ?? {};
|
|
2680
|
+
const out = { ...input };
|
|
2681
|
+
if (out.orgx_plugin_version === undefined) {
|
|
2682
|
+
out.orgx_plugin_version = (config.pluginVersion ?? "").trim() || null;
|
|
2683
|
+
}
|
|
2684
|
+
try {
|
|
2685
|
+
const state = readSkillPackState();
|
|
2686
|
+
const overrides = state.overrides;
|
|
2687
|
+
if (out.skill_pack_name === undefined) {
|
|
2688
|
+
out.skill_pack_name = overrides?.name ?? state.pack?.name ?? null;
|
|
2689
|
+
}
|
|
2690
|
+
if (out.skill_pack_version === undefined) {
|
|
2691
|
+
out.skill_pack_version = overrides?.version ?? state.pack?.version ?? null;
|
|
2692
|
+
}
|
|
2693
|
+
if (out.skill_pack_checksum === undefined) {
|
|
2694
|
+
out.skill_pack_checksum = overrides?.checksum ?? state.pack?.checksum ?? null;
|
|
2695
|
+
}
|
|
2696
|
+
if (out.skill_pack_source === undefined) {
|
|
2697
|
+
out.skill_pack_source = overrides?.source ?? null;
|
|
2698
|
+
}
|
|
2699
|
+
if (out.skill_pack_etag === undefined) {
|
|
2700
|
+
out.skill_pack_etag = state.etag ?? null;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
catch {
|
|
2704
|
+
// best effort
|
|
2705
|
+
}
|
|
2706
|
+
if (out.orgx_provenance === undefined) {
|
|
2707
|
+
out.orgx_provenance = {
|
|
2708
|
+
plugin_version: out.orgx_plugin_version ?? null,
|
|
2709
|
+
skill_pack: {
|
|
2710
|
+
name: out.skill_pack_name ?? null,
|
|
2711
|
+
version: out.skill_pack_version ?? null,
|
|
2712
|
+
checksum: out.skill_pack_checksum ?? null,
|
|
2713
|
+
source: out.skill_pack_source ?? null,
|
|
2714
|
+
etag: out.skill_pack_etag ?? null,
|
|
2715
|
+
},
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
return out;
|
|
2719
|
+
}
|
|
1804
2720
|
async function emitActivityWithFallback(source, payload) {
|
|
1805
2721
|
if (!payload.message || payload.message.trim().length === 0) {
|
|
1806
2722
|
return text("❌ message is required");
|
|
@@ -1821,10 +2737,10 @@ export default function register(api) {
|
|
|
1821
2737
|
progress_pct: payload.progress_pct,
|
|
1822
2738
|
level: payload.level ?? "info",
|
|
1823
2739
|
next_step: payload.next_step,
|
|
1824
|
-
metadata: {
|
|
2740
|
+
metadata: withProvenanceMetadata({
|
|
1825
2741
|
...(payload.metadata ?? {}),
|
|
1826
2742
|
source,
|
|
1827
|
-
},
|
|
2743
|
+
}),
|
|
1828
2744
|
};
|
|
1829
2745
|
const activityItem = {
|
|
1830
2746
|
id,
|
|
@@ -1887,10 +2803,10 @@ export default function register(api) {
|
|
|
1887
2803
|
timestamp: now,
|
|
1888
2804
|
phase: "review",
|
|
1889
2805
|
summary: `${payload.operations.length} operation${payload.operations.length === 1 ? "" : "s"}`,
|
|
1890
|
-
metadata: {
|
|
2806
|
+
metadata: withProvenanceMetadata({
|
|
1891
2807
|
source,
|
|
1892
2808
|
idempotency_key: idempotencyKey,
|
|
1893
|
-
},
|
|
2809
|
+
}),
|
|
1894
2810
|
};
|
|
1895
2811
|
try {
|
|
1896
2812
|
const result = await client.applyChangeset(requestPayload);
|
|
@@ -1908,7 +2824,7 @@ export default function register(api) {
|
|
|
1908
2824
|
}
|
|
1909
2825
|
}
|
|
1910
2826
|
// --- orgx_emit_activity ---
|
|
1911
|
-
|
|
2827
|
+
registerMcpTool({
|
|
1912
2828
|
name: "orgx_emit_activity",
|
|
1913
2829
|
description: "Emit append-only OrgX activity telemetry (launch reporting contract primary write tool).",
|
|
1914
2830
|
parameters: {
|
|
@@ -1968,7 +2884,7 @@ export default function register(api) {
|
|
|
1968
2884
|
},
|
|
1969
2885
|
}, { optional: true });
|
|
1970
2886
|
// --- orgx_apply_changeset ---
|
|
1971
|
-
|
|
2887
|
+
registerMcpTool({
|
|
1972
2888
|
name: "orgx_apply_changeset",
|
|
1973
2889
|
description: "Apply an idempotent transactional OrgX changeset (launch reporting contract primary mutation tool).",
|
|
1974
2890
|
parameters: {
|
|
@@ -2011,7 +2927,7 @@ export default function register(api) {
|
|
|
2011
2927
|
},
|
|
2012
2928
|
}, { optional: true });
|
|
2013
2929
|
// --- orgx_report_progress (alias -> orgx_emit_activity) ---
|
|
2014
|
-
|
|
2930
|
+
registerMcpTool({
|
|
2015
2931
|
name: "orgx_report_progress",
|
|
2016
2932
|
description: "Alias for orgx_emit_activity. Report progress at key milestones so the team can track your work.",
|
|
2017
2933
|
parameters: {
|
|
@@ -2074,7 +2990,7 @@ export default function register(api) {
|
|
|
2074
2990
|
},
|
|
2075
2991
|
}, { optional: true });
|
|
2076
2992
|
// --- orgx_request_decision (alias -> orgx_apply_changeset decision.create) ---
|
|
2077
|
-
|
|
2993
|
+
registerMcpTool({
|
|
2078
2994
|
name: "orgx_request_decision",
|
|
2079
2995
|
description: "Alias for orgx_apply_changeset with decision.create. Request a human decision before proceeding.",
|
|
2080
2996
|
parameters: {
|
|
@@ -2159,12 +3075,16 @@ export default function register(api) {
|
|
|
2159
3075
|
},
|
|
2160
3076
|
}, { optional: true });
|
|
2161
3077
|
// --- orgx_register_artifact ---
|
|
2162
|
-
|
|
3078
|
+
registerMcpTool({
|
|
2163
3079
|
name: "orgx_register_artifact",
|
|
2164
3080
|
description: "Register a work output (PR, document, config change, report, etc.) with OrgX. Makes it visible in the dashboard.",
|
|
2165
3081
|
parameters: {
|
|
2166
3082
|
type: "object",
|
|
2167
3083
|
properties: {
|
|
3084
|
+
initiative_id: {
|
|
3085
|
+
type: "string",
|
|
3086
|
+
description: "Optional initiative UUID to attach this artifact to",
|
|
3087
|
+
},
|
|
2168
3088
|
name: {
|
|
2169
3089
|
type: "string",
|
|
2170
3090
|
description: "Human-readable artifact name (e.g., 'PR #107: Fix build size')",
|
|
@@ -2189,6 +3109,9 @@ export default function register(api) {
|
|
|
2189
3109
|
async execute(_callId, params = { name: "", artifact_type: "other" }) {
|
|
2190
3110
|
const now = new Date().toISOString();
|
|
2191
3111
|
const id = `artifact:${randomUUID().slice(0, 8)}`;
|
|
3112
|
+
const initiativeId = isUuid(params.initiative_id)
|
|
3113
|
+
? params.initiative_id
|
|
3114
|
+
: inferReportingInitiativeId(params) ?? null;
|
|
2192
3115
|
const activityItem = {
|
|
2193
3116
|
id,
|
|
2194
3117
|
type: "artifact_created",
|
|
@@ -2197,20 +3120,21 @@ export default function register(api) {
|
|
|
2197
3120
|
agentId: null,
|
|
2198
3121
|
agentName: null,
|
|
2199
3122
|
runId: null,
|
|
2200
|
-
initiativeId
|
|
3123
|
+
initiativeId,
|
|
2201
3124
|
timestamp: now,
|
|
2202
3125
|
summary: params.url ?? null,
|
|
2203
|
-
metadata: {
|
|
3126
|
+
metadata: withProvenanceMetadata({
|
|
2204
3127
|
source: "orgx_register_artifact",
|
|
2205
3128
|
artifact_type: params.artifact_type,
|
|
2206
3129
|
url: params.url,
|
|
2207
|
-
},
|
|
3130
|
+
}),
|
|
2208
3131
|
};
|
|
2209
3132
|
try {
|
|
2210
3133
|
const entity = await client.createEntity("artifact", {
|
|
2211
|
-
|
|
3134
|
+
title: params.name,
|
|
2212
3135
|
artifact_type: params.artifact_type,
|
|
2213
|
-
|
|
3136
|
+
summary: params.description,
|
|
3137
|
+
initiative_id: initiativeId ?? undefined,
|
|
2214
3138
|
artifact_url: params.url,
|
|
2215
3139
|
status: "active",
|
|
2216
3140
|
});
|
|
@@ -2221,7 +3145,10 @@ export default function register(api) {
|
|
|
2221
3145
|
id,
|
|
2222
3146
|
type: "artifact",
|
|
2223
3147
|
timestamp: now,
|
|
2224
|
-
payload:
|
|
3148
|
+
payload: {
|
|
3149
|
+
...params,
|
|
3150
|
+
initiative_id: initiativeId,
|
|
3151
|
+
},
|
|
2225
3152
|
activityItem,
|
|
2226
3153
|
});
|
|
2227
3154
|
return text(`Artifact saved locally: ${params.name} [${params.artifact_type}] (will sync when connected)`);
|
|
@@ -2332,7 +3259,39 @@ export default function register(api) {
|
|
|
2332
3259
|
}, {
|
|
2333
3260
|
getHealth: async (input = {}) => buildHealthReport({ probeRemote: input.probeRemote === true }),
|
|
2334
3261
|
});
|
|
2335
|
-
|
|
3262
|
+
const mcpPromptRegistry = new Map();
|
|
3263
|
+
mcpPromptRegistry.set("ship", {
|
|
3264
|
+
name: "ship",
|
|
3265
|
+
description: "Commit local changes, open a PR, and merge it (GitHub CLI required).",
|
|
3266
|
+
arguments: [],
|
|
3267
|
+
messages: [
|
|
3268
|
+
{
|
|
3269
|
+
role: "user",
|
|
3270
|
+
content: [
|
|
3271
|
+
"Ship the current work:",
|
|
3272
|
+
"- Inspect `git status -sb` and `git diff --stat` and summarize what will be shipped.",
|
|
3273
|
+
"- Run `npm run typecheck`, `npm run test:hooks`, and `npm run build` (fix failures).",
|
|
3274
|
+
"- Create a feature branch if on `main`.",
|
|
3275
|
+
"- Commit with a clear message (do not include secrets).",
|
|
3276
|
+
"- Push branch, open a PR (use `gh pr create`), then merge it (use `gh pr merge --merge --auto`).",
|
|
3277
|
+
"- If `gh` is not authenticated, stop and tell me what to run.",
|
|
3278
|
+
].join("\n"),
|
|
3279
|
+
},
|
|
3280
|
+
],
|
|
3281
|
+
});
|
|
3282
|
+
const mcpHttpHandler = createMcpHttpHandler({
|
|
3283
|
+
tools: mcpToolRegistry,
|
|
3284
|
+
prompts: mcpPromptRegistry,
|
|
3285
|
+
logger: api.log ?? {},
|
|
3286
|
+
serverName: "@useorgx/openclaw-plugin",
|
|
3287
|
+
serverVersion: config.pluginVersion,
|
|
3288
|
+
});
|
|
3289
|
+
const compositeHttpHandler = async (req, res) => {
|
|
3290
|
+
if (await mcpHttpHandler(req, res))
|
|
3291
|
+
return true;
|
|
3292
|
+
return await httpHandler(req, res);
|
|
3293
|
+
};
|
|
3294
|
+
api.registerHttpHandler(compositeHttpHandler);
|
|
2336
3295
|
api.log?.info?.("[orgx] Plugin registered", {
|
|
2337
3296
|
baseUrl: config.baseUrl,
|
|
2338
3297
|
hasApiKey: !!config.apiKey,
|