@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.
Files changed (51) hide show
  1. package/README.md +85 -2
  2. package/dashboard/dist/assets/0tOC3wSN.js +214 -0
  3. package/dashboard/dist/assets/Bm8QnMJ_.js +1 -0
  4. package/dashboard/dist/assets/CpJsfbXo.js +9 -0
  5. package/dashboard/dist/assets/CyxZio4Y.js +1 -0
  6. package/dashboard/dist/assets/DaAIOik3.css +1 -0
  7. package/dashboard/dist/index.html +3 -3
  8. package/dist/activity-store.d.ts +28 -0
  9. package/dist/activity-store.js +250 -0
  10. package/dist/agent-context-store.d.ts +19 -0
  11. package/dist/agent-context-store.js +60 -3
  12. package/dist/agent-suite.d.ts +83 -0
  13. package/dist/agent-suite.js +615 -0
  14. package/dist/contracts/client.d.ts +22 -1
  15. package/dist/contracts/client.js +120 -3
  16. package/dist/contracts/types.d.ts +190 -1
  17. package/dist/entity-comment-store.d.ts +29 -0
  18. package/dist/entity-comment-store.js +190 -0
  19. package/dist/hooks/post-reporting-event.mjs +326 -0
  20. package/dist/http-handler.d.ts +7 -1
  21. package/dist/http-handler.js +3619 -585
  22. package/dist/index.js +1039 -80
  23. package/dist/mcp-client-setup.d.ts +30 -0
  24. package/dist/mcp-client-setup.js +347 -0
  25. package/dist/mcp-http-handler.d.ts +55 -0
  26. package/dist/mcp-http-handler.js +395 -0
  27. package/dist/next-up-queue-store.d.ts +31 -0
  28. package/dist/next-up-queue-store.js +169 -0
  29. package/dist/openclaw.plugin.json +1 -1
  30. package/dist/outbox.d.ts +1 -1
  31. package/dist/runtime-instance-store.d.ts +1 -1
  32. package/dist/runtime-instance-store.js +20 -3
  33. package/dist/skill-pack-state.d.ts +69 -0
  34. package/dist/skill-pack-state.js +232 -0
  35. package/dist/worker-supervisor.d.ts +25 -0
  36. package/dist/worker-supervisor.js +62 -0
  37. package/openclaw.plugin.json +1 -1
  38. package/package.json +10 -1
  39. package/skills/orgx-design-agent/SKILL.md +38 -0
  40. package/skills/orgx-engineering-agent/SKILL.md +55 -0
  41. package/skills/orgx-marketing-agent/SKILL.md +40 -0
  42. package/skills/orgx-operations-agent/SKILL.md +40 -0
  43. package/skills/orgx-orchestrator-agent/SKILL.md +45 -0
  44. package/skills/orgx-product-agent/SKILL.md +39 -0
  45. package/skills/orgx-sales-agent/SKILL.md +40 -0
  46. package/skills/ship/SKILL.md +63 -0
  47. package/dashboard/dist/assets/4hvaB0UC.js +0 -9
  48. package/dashboard/dist/assets/BgsfM2lz.js +0 -1
  49. package/dashboard/dist/assets/DCBlK4MX.js +0 -212
  50. package/dashboard/dist/assets/DEuY_RBN.js +0 -1
  51. 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 { readFileSync } from "node:fs";
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
- const baseApiUrl = config.baseUrl.replace(/\/+$/, "");
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
- const initiativeId = pickNonEmptyString(input.initiative_id, process.env.ORGX_INITIATIVE_ID);
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, defaultReportingCorrelationId, `openclaw-${Date.now()}`);
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
- return new URL("/settings", baseApiUrl).toString();
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 response = await fetch(`${baseApiUrl}${path}`, {
568
- method,
569
- headers: {
570
- "Content-Type": "application/json",
571
- },
572
- body: body ? JSON.stringify(body) : undefined,
573
- });
574
- const payload = (await response.json().catch(() => null));
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
- return { ok: false, status: response.status, error: errorMessage };
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
- return { ok: true, data: payload };
755
+ if (payload !== null) {
756
+ return { ok: true, data: payload };
757
+ }
758
+ return { ok: true, data: rawText };
596
759
  }
597
760
  catch (err) {
598
- return { ok: false, status: 0, error: toErrorMessage(err) };
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
- await client.getOrgSnapshot();
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" ? payload.progress_pct : undefined;
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
- await client.emitActivity({
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") ?? undefined,
831
- metadata: {
832
- source: "orgx_openclaw_outbox_replay",
833
- outbox_event_id: event.id,
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: context.value.runId,
853
- correlation_id: context.value.correlationId,
1408
+ run_id: runFields.run_id,
1409
+ correlation_id: runFields.correlation_id,
854
1410
  source_client: context.value.sourceClient,
855
- idempotency_key: pickStringField(payload, "idempotency_key") ?? `decision:${event.id}`,
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: context.value.runId,
886
- correlation_id: context.value.correlationId,
1506
+ run_id: runFields.run_id,
1507
+ correlation_id: runFields.correlation_id,
887
1508
  source_client: context.value.sourceClient,
888
- idempotency_key: pickStringField(payload, "idempotency_key") ?? `changeset:${event.id}`,
1509
+ idempotency_key: resolvedIdempotencyKey,
889
1510
  operations,
890
1511
  });
891
1512
  return;
892
1513
  }
893
- if (event.type === "artifact") {
894
- const name = pickStringField(payload, "name");
895
- if (!name) {
896
- api.log?.warn?.("[orgx] Dropping invalid artifact outbox event", {
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
- await client.createEntity("artifact", {
902
- name,
903
- artifact_type: pickStringField(payload, "artifact_type") ?? "other",
904
- description: pickStringField(payload, "description"),
905
- artifact_url: pickStringField(payload, "url"),
906
- status: "active",
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
- for (const queue of outboxQueues) {
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
- updateCachedSnapshot(await client.getOrgSnapshot());
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: true,
989
- lastError: null,
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: toErrorMessage(err),
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 message = `Pairing start failed: ${started.error}`;
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
- api.registerTool({
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
- api.registerTool({
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
- api.registerTool({
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
- api.registerTool({
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
- api.registerTool({
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
- api.registerTool({
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
- api.registerTool({
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
- api.registerTool({
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
- api.registerTool({
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
- api.registerTool({
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
- api.registerTool({
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
- api.registerTool({
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
- api.registerTool({
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
- api.registerTool({
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
- api.registerTool({
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
- api.registerTool({
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: null,
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
- name: params.name,
3134
+ title: params.name,
2212
3135
  artifact_type: params.artifact_type,
2213
- description: params.description,
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: params,
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
- api.registerHttpHandler(httpHandler);
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,