@useorgx/openclaw-plugin 0.4.5 → 0.4.8

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 (60) hide show
  1. package/README.md +333 -26
  2. package/dashboard/dist/assets/B3ziCA02.js +8 -0
  3. package/dashboard/dist/assets/BNeJ0kpF.js +1 -0
  4. package/dashboard/dist/assets/BzkiMPmM.js +215 -0
  5. package/dashboard/dist/assets/CUV9IHHi.js +1 -0
  6. package/dashboard/dist/assets/CpJsfbXo.js +9 -0
  7. package/dashboard/dist/assets/Ie7d9Iq2.css +1 -0
  8. package/dashboard/dist/assets/sAhvFnpk.js +4 -0
  9. package/dashboard/dist/index.html +5 -5
  10. package/dist/activity-actor-fields.d.ts +3 -0
  11. package/dist/activity-actor-fields.js +128 -0
  12. package/dist/activity-store.d.ts +28 -0
  13. package/dist/activity-store.js +257 -0
  14. package/dist/agent-context-store.d.ts +19 -0
  15. package/dist/agent-context-store.js +60 -3
  16. package/dist/agent-suite.d.ts +83 -0
  17. package/dist/agent-suite.js +615 -0
  18. package/dist/artifacts/register-artifact.d.ts +47 -0
  19. package/dist/artifacts/register-artifact.js +271 -0
  20. package/dist/auth-store.js +8 -13
  21. package/dist/contracts/client.d.ts +23 -1
  22. package/dist/contracts/client.js +127 -8
  23. package/dist/contracts/types.d.ts +194 -1
  24. package/dist/entity-comment-store.d.ts +29 -0
  25. package/dist/entity-comment-store.js +190 -0
  26. package/dist/hooks/post-reporting-event.mjs +326 -0
  27. package/dist/http-handler.d.ts +7 -1
  28. package/dist/http-handler.js +4500 -534
  29. package/dist/index.js +1078 -68
  30. package/dist/local-openclaw.js +8 -0
  31. package/dist/mcp-client-setup.js +145 -28
  32. package/dist/mcp-http-handler.d.ts +17 -0
  33. package/dist/mcp-http-handler.js +144 -3
  34. package/dist/next-up-queue-store.d.ts +31 -0
  35. package/dist/next-up-queue-store.js +169 -0
  36. package/dist/openclaw.plugin.json +1 -1
  37. package/dist/outbox.d.ts +1 -1
  38. package/dist/runtime-instance-store.d.ts +1 -1
  39. package/dist/runtime-instance-store.js +19 -2
  40. package/dist/skill-pack-state.d.ts +69 -0
  41. package/dist/skill-pack-state.js +232 -0
  42. package/dist/worker-supervisor.d.ts +25 -0
  43. package/dist/worker-supervisor.js +77 -0
  44. package/openclaw.plugin.json +1 -1
  45. package/package.json +15 -1
  46. package/skills/orgx-design-agent/SKILL.md +38 -0
  47. package/skills/orgx-engineering-agent/SKILL.md +55 -0
  48. package/skills/orgx-marketing-agent/SKILL.md +40 -0
  49. package/skills/orgx-operations-agent/SKILL.md +40 -0
  50. package/skills/orgx-orchestrator-agent/SKILL.md +45 -0
  51. package/skills/orgx-product-agent/SKILL.md +39 -0
  52. package/skills/orgx-sales-agent/SKILL.md +40 -0
  53. package/skills/ship/SKILL.md +63 -0
  54. package/dashboard/dist/assets/B68j2crt.js +0 -1
  55. package/dashboard/dist/assets/BZZ-fiJx.js +0 -32
  56. package/dashboard/dist/assets/BoXlCHKa.js +0 -9
  57. package/dashboard/dist/assets/Bq9x_Xyh.css +0 -1
  58. package/dashboard/dist/assets/DBhrRVdp.js +0 -1
  59. package/dashboard/dist/assets/DD1jv1Hd.js +0 -8
  60. package/dashboard/dist/assets/DNjbmawF.js +0 -214
package/dist/index.js CHANGED
@@ -12,20 +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 { registerArtifact } from "./artifacts/register-artifact.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";
25
- import { createMcpHttpHandler } from "./mcp-http-handler.js";
29
+ import { createMcpHttpHandler, } from "./mcp-http-handler.js";
26
30
  import { autoConfigureDetectedMcpClients } from "./mcp-client-setup.js";
27
31
  import { readOpenClawGatewayPort, readOpenClawSettingsSnapshot } from "./openclaw-settings.js";
28
32
  import { posthogCapture } from "./telemetry/posthog.js";
33
+ import { readSkillPackState, refreshSkillPackState } from "./skill-pack-state.js";
29
34
  export { OrgXClient } from "./api.js";
30
35
  const DEFAULT_BASE_URL = "https://www.useorgx.com";
31
36
  const DEFAULT_DOCS_URL = "https://orgx.mintlify.site/guides/openclaw-plugin-setup";
@@ -34,6 +39,16 @@ function isUserScopedApiKey(apiKey) {
34
39
  }
35
40
  function resolveRuntimeUserId(apiKey, candidates) {
36
41
  if (isUserScopedApiKey(apiKey)) {
42
+ // For oxk_ keys, the OrgX API ignores X-Orgx-User-Id, but we still keep a UUID
43
+ // around for created_by_id on certain entity writes (e.g., work_artifacts).
44
+ for (const candidate of candidates) {
45
+ if (typeof candidate !== "string")
46
+ continue;
47
+ const trimmed = candidate.trim();
48
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(trimmed)) {
49
+ return trimmed;
50
+ }
51
+ }
37
52
  return "";
38
53
  }
39
54
  for (const candidate of candidates) {
@@ -186,6 +201,7 @@ function resolveConfig(api, input) {
186
201
  baseUrl,
187
202
  syncIntervalMs: pluginConf.syncIntervalMs ?? 300_000,
188
203
  enabled: pluginConf.enabled ?? true,
204
+ autoInstallAgentSuiteOnConnect: pluginConf.autoInstallAgentSuiteOnConnect ?? true,
189
205
  dashboardEnabled: pluginConf.dashboardEnabled ?? true,
190
206
  installationId: input.installationId,
191
207
  pluginVersion: resolvePluginVersion(),
@@ -268,6 +284,29 @@ function isUuid(value) {
268
284
  return false;
269
285
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
270
286
  }
287
+ function inferReportingInitiativeId(input) {
288
+ const env = pickNonEmptyString(process.env.ORGX_INITIATIVE_ID);
289
+ if (isUuid(env))
290
+ return env;
291
+ const agentId = pickNonEmptyString(input.agent_id, input.agentId);
292
+ if (agentId) {
293
+ const ctx = getAgentContext(agentId);
294
+ const ctxInit = ctx?.initiativeId ?? undefined;
295
+ if (isUuid(ctxInit ?? undefined))
296
+ return ctxInit ?? undefined;
297
+ }
298
+ // Fall back to the most recently updated agent context with a UUID initiative id.
299
+ try {
300
+ const store = readAgentContexts();
301
+ const candidates = Object.values(store.agents ?? {}).filter((ctx) => isUuid(ctx?.initiativeId ?? undefined));
302
+ candidates.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
303
+ const picked = candidates[0]?.initiativeId ?? undefined;
304
+ return isUuid(picked) ? picked : undefined;
305
+ }
306
+ catch {
307
+ return undefined;
308
+ }
309
+ }
271
310
  function toReportingPhase(phase, progressPct) {
272
311
  if (progressPct === 100)
273
312
  return "completed";
@@ -363,11 +402,66 @@ export default function register(api) {
363
402
  pollIntervalMs: null,
364
403
  };
365
404
  let activePairing = null;
366
- const baseApiUrl = config.baseUrl.replace(/\/+$/, "");
405
+ // NOTE: base URL can be updated at runtime (e.g. user edits OpenClaw config). Keep it mutable.
406
+ let baseApiUrl = config.baseUrl.replace(/\/+$/, "");
367
407
  const defaultReportingCorrelationId = pickNonEmptyString(process.env.ORGX_CORRELATION_ID) ??
368
408
  `openclaw-${config.installationId}`;
409
+ function refreshConfigFromSources(input) {
410
+ const allowApiKeyChanges = input?.allowApiKeyChanges !== false;
411
+ const previousApiKey = config.apiKey;
412
+ const previousBaseUrl = config.baseUrl;
413
+ const previousUserId = config.userId;
414
+ const previousDocsUrl = config.docsUrl;
415
+ const previousKeySource = config.apiKeySource;
416
+ const latestPersisted = loadAuthStore();
417
+ const next = resolveConfig(api, {
418
+ installationId: config.installationId,
419
+ persistedApiKey: latestPersisted?.apiKey ?? null,
420
+ persistedUserId: latestPersisted?.userId ?? null,
421
+ });
422
+ const nextApiKey = allowApiKeyChanges ? next.apiKey : previousApiKey;
423
+ const nextUserId = allowApiKeyChanges ? next.userId : previousUserId;
424
+ const changed = nextApiKey !== previousApiKey ||
425
+ next.baseUrl !== previousBaseUrl ||
426
+ nextUserId !== previousUserId ||
427
+ next.docsUrl !== previousDocsUrl ||
428
+ next.apiKeySource !== previousKeySource;
429
+ if (!changed) {
430
+ return false;
431
+ }
432
+ if (allowApiKeyChanges) {
433
+ config.apiKey = nextApiKey;
434
+ config.userId = nextUserId;
435
+ config.apiKeySource = next.apiKeySource;
436
+ }
437
+ config.baseUrl = next.baseUrl;
438
+ config.docsUrl = next.docsUrl;
439
+ baseApiUrl = config.baseUrl.replace(/\/+$/, "");
440
+ client.setCredentials({
441
+ apiKey: config.apiKey,
442
+ userId: config.userId,
443
+ baseUrl: config.baseUrl,
444
+ });
445
+ // Keep onboarding state aligned with what's actually configured (without forcing a status transition).
446
+ updateOnboardingState({
447
+ hasApiKey: Boolean(config.apiKey),
448
+ keySource: config.apiKeySource,
449
+ docsUrl: config.docsUrl,
450
+ installationId: config.installationId,
451
+ });
452
+ api.log?.info?.("[orgx] Config refreshed", {
453
+ reason: input?.reason ?? "runtime_refresh",
454
+ baseUrl: config.baseUrl,
455
+ hasApiKey: Boolean(config.apiKey),
456
+ apiKeySource: config.apiKeySource,
457
+ });
458
+ return true;
459
+ }
369
460
  function resolveReportingContext(input) {
370
- const initiativeId = pickNonEmptyString(input.initiative_id, input.initiativeId, process.env.ORGX_INITIATIVE_ID);
461
+ let initiativeId = pickNonEmptyString(input.initiative_id, input.initiativeId, process.env.ORGX_INITIATIVE_ID);
462
+ if (!isUuid(initiativeId)) {
463
+ initiativeId = inferReportingInitiativeId(input);
464
+ }
371
465
  if (!initiativeId || !isUuid(initiativeId)) {
372
466
  return {
373
467
  ok: false,
@@ -411,6 +505,16 @@ export default function register(api) {
411
505
  return err.message;
412
506
  return typeof err === "string" ? err : "Unexpected error";
413
507
  }
508
+ function stableHash(value) {
509
+ return createHash("sha256").update(value).digest("hex");
510
+ }
511
+ function isAuthFailure(err) {
512
+ const message = toErrorMessage(err).toLowerCase();
513
+ return (message.includes("401") ||
514
+ message.includes("unauthorized") ||
515
+ message.includes("invalid_token") ||
516
+ message.includes("invalid api key"));
517
+ }
414
518
  const registerTool = api.registerTool.bind(api);
415
519
  api.registerTool = (tool, options) => {
416
520
  const toolName = tool.name;
@@ -562,22 +666,47 @@ export default function register(api) {
562
666
  }
563
667
  function buildManualKeyConnectUrl() {
564
668
  try {
565
- return new URL("/settings", baseApiUrl).toString();
669
+ // Deep-link into the Security section where API keys live.
670
+ return new URL("/settings#security", baseApiUrl).toString();
566
671
  }
567
672
  catch {
568
- return "https://www.useorgx.com/settings";
673
+ return "https://www.useorgx.com/settings#security";
569
674
  }
570
675
  }
571
- async function fetchOrgxJson(method, path, body) {
676
+ async function fetchOrgxJson(method, path, body, options) {
572
677
  try {
573
- const response = await fetch(`${baseApiUrl}${path}`, {
574
- method,
575
- headers: {
576
- "Content-Type": "application/json",
577
- },
578
- body: body ? JSON.stringify(body) : undefined,
579
- });
580
- const payload = (await response.json().catch(() => null));
678
+ const controller = new AbortController();
679
+ const timeoutMs = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs)
680
+ ? Math.max(1_000, Math.floor(options.timeoutMs))
681
+ : 12_000;
682
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
683
+ let response;
684
+ let rawText = "";
685
+ try {
686
+ response = await fetch(`${baseApiUrl}${path}`, {
687
+ method,
688
+ signal: controller.signal,
689
+ headers: {
690
+ Accept: "application/json",
691
+ "Content-Type": "application/json",
692
+ },
693
+ body: body ? JSON.stringify(body) : undefined,
694
+ });
695
+ rawText = await response.text().catch(() => "");
696
+ }
697
+ finally {
698
+ clearTimeout(timeout);
699
+ }
700
+ const payload = (() => {
701
+ if (!rawText)
702
+ return null;
703
+ try {
704
+ return JSON.parse(rawText);
705
+ }
706
+ catch {
707
+ return null;
708
+ }
709
+ })();
581
710
  if (!response.ok) {
582
711
  const rawError = payload?.error ?? payload?.message;
583
712
  let errorMessage;
@@ -590,18 +719,62 @@ export default function register(api) {
590
719
  typeof rawError.message === "string") {
591
720
  errorMessage = rawError.message;
592
721
  }
722
+ else if (rawText && rawText.trim().length > 0) {
723
+ // Avoid dumping HTML (Cloudflare / Next.js error pages) into UI; keep it short.
724
+ const sanitized = rawText
725
+ .replace(/\s+/g, " ")
726
+ .replace(/<[^>]+>/g, "")
727
+ .trim();
728
+ errorMessage = sanitized.length > 0 ? sanitized.slice(0, 180) : `OrgX request failed (${response.status})`;
729
+ }
593
730
  else {
594
731
  errorMessage = `OrgX request failed (${response.status})`;
595
732
  }
596
- return { ok: false, status: response.status, error: errorMessage };
733
+ const statusToken = `HTTP ${response.status}`;
734
+ if (response.status &&
735
+ !errorMessage.toLowerCase().includes(statusToken.toLowerCase()) &&
736
+ !errorMessage.includes(`(${response.status})`)) {
737
+ errorMessage = `${errorMessage} (HTTP ${response.status})`;
738
+ }
739
+ const debugParts = [];
740
+ const requestId = response.headers.get("x-request-id");
741
+ const vercelId = response.headers.get("x-vercel-id");
742
+ const cfRay = response.headers.get("cf-ray");
743
+ const clerkStatus = response.headers.get("x-clerk-auth-status");
744
+ const clerkReason = response.headers.get("x-clerk-auth-reason");
745
+ if (requestId)
746
+ debugParts.push(`req=${requestId}`);
747
+ if (vercelId && vercelId !== requestId)
748
+ debugParts.push(`vercel=${vercelId}`);
749
+ if (cfRay)
750
+ debugParts.push(`cf-ray=${cfRay}`);
751
+ if (clerkStatus)
752
+ debugParts.push(`clerk=${clerkStatus}`);
753
+ if (clerkReason)
754
+ debugParts.push(`clerk_reason=${clerkReason}`);
755
+ const debugSuffix = debugParts.length > 0 ? ` (${debugParts.join(", ")})` : "";
756
+ return {
757
+ ok: false,
758
+ status: response.status,
759
+ error: `${errorMessage}${debugSuffix}`,
760
+ };
597
761
  }
598
762
  if (payload?.data !== undefined) {
599
763
  return { ok: true, data: payload.data };
600
764
  }
601
- return { ok: true, data: payload };
765
+ if (payload !== null) {
766
+ return { ok: true, data: payload };
767
+ }
768
+ return { ok: true, data: rawText };
602
769
  }
603
770
  catch (err) {
604
- return { ok: false, status: 0, error: toErrorMessage(err) };
771
+ const message = err &&
772
+ typeof err === "object" &&
773
+ "name" in err &&
774
+ err.name === "AbortError"
775
+ ? `OrgX request timed out (method=${method}, path=${path})`
776
+ : toErrorMessage(err);
777
+ return { ok: false, status: 0, error: message };
605
778
  }
606
779
  }
607
780
  function setRuntimeApiKey(input) {
@@ -664,6 +837,7 @@ export default function register(api) {
664
837
  const probeRemote = input.probeRemote === true;
665
838
  const outbox = await readOutboxSummary();
666
839
  const checks = [];
840
+ refreshConfigFromSources({ reason: "health_check" });
667
841
  const hasApiKey = Boolean(config.apiKey);
668
842
  if (hasApiKey) {
669
843
  checks.push({
@@ -721,7 +895,9 @@ export default function register(api) {
721
895
  else {
722
896
  const startedAt = Date.now();
723
897
  try {
724
- await client.getOrgSnapshot();
898
+ // Avoid probing with /api/client/sync: it's heavier than necessary and can
899
+ // create false negatives during transient server slowness.
900
+ await client.getBillingStatus();
725
901
  remoteReachable = true;
726
902
  remoteLatencyMs = Date.now() - startedAt;
727
903
  checks.push({
@@ -816,8 +992,313 @@ export default function register(api) {
816
992
  .filter(Boolean);
817
993
  return strings.length > 0 ? strings : undefined;
818
994
  }
995
+ function isPidAlive(pid) {
996
+ if (!Number.isFinite(pid) || !pid || pid <= 0)
997
+ return false;
998
+ try {
999
+ process.kill(pid, 0);
1000
+ return true;
1001
+ }
1002
+ catch {
1003
+ return false;
1004
+ }
1005
+ }
1006
+ function toFiniteNumber(value) {
1007
+ if (typeof value === "number" && Number.isFinite(value))
1008
+ return value;
1009
+ if (typeof value === "string" && value.trim().length > 0) {
1010
+ const parsed = Number(value);
1011
+ if (Number.isFinite(parsed))
1012
+ return parsed;
1013
+ }
1014
+ return null;
1015
+ }
1016
+ function isSafePathSegment(value) {
1017
+ const normalized = value.trim();
1018
+ if (!normalized || normalized === "." || normalized === "..")
1019
+ return false;
1020
+ if (normalized.includes("/") || normalized.includes("\\") || normalized.includes("\0")) {
1021
+ return false;
1022
+ }
1023
+ if (normalized.includes(".."))
1024
+ return false;
1025
+ return true;
1026
+ }
1027
+ function parseRetroEntityType(value) {
1028
+ if (!value)
1029
+ return undefined;
1030
+ switch (value) {
1031
+ case "initiative":
1032
+ case "workstream":
1033
+ case "milestone":
1034
+ case "task":
1035
+ return value;
1036
+ default:
1037
+ return undefined;
1038
+ }
1039
+ }
1040
+ function readOpenClawSessionSummary(input) {
1041
+ const agentId = input.agentId.trim();
1042
+ const sessionId = input.sessionId.trim();
1043
+ if (!agentId || !sessionId) {
1044
+ return { tokens: 0, costUsd: 0, hadError: false, errorMessage: null };
1045
+ }
1046
+ if (!isSafePathSegment(agentId) || !isSafePathSegment(sessionId)) {
1047
+ return { tokens: 0, costUsd: 0, hadError: false, errorMessage: null };
1048
+ }
1049
+ const jsonlPath = join(homedir(), ".openclaw", "agents", agentId, "sessions", `${sessionId}.jsonl`);
1050
+ try {
1051
+ if (!existsSync(jsonlPath)) {
1052
+ return { tokens: 0, costUsd: 0, hadError: false, errorMessage: null };
1053
+ }
1054
+ const raw = readFileSync(jsonlPath, "utf8");
1055
+ const lines = raw.split("\n");
1056
+ let tokens = 0;
1057
+ let costUsd = 0;
1058
+ let hadError = false;
1059
+ let errorMessage = null;
1060
+ for (const line of lines) {
1061
+ const trimmed = line.trim();
1062
+ if (!trimmed)
1063
+ continue;
1064
+ try {
1065
+ const evt = JSON.parse(trimmed);
1066
+ if (evt.type !== "message")
1067
+ continue;
1068
+ const msg = evt.message;
1069
+ if (!msg || typeof msg !== "object")
1070
+ continue;
1071
+ const usage = msg.usage;
1072
+ if (usage && typeof usage === "object") {
1073
+ const totalTokens = toFiniteNumber(usage.totalTokens) ??
1074
+ toFiniteNumber(usage.total_tokens) ??
1075
+ null;
1076
+ const inputTokens = toFiniteNumber(usage.input) ?? 0;
1077
+ const outputTokens = toFiniteNumber(usage.output) ?? 0;
1078
+ const cacheReadTokens = toFiniteNumber(usage.cacheRead) ?? 0;
1079
+ const cacheWriteTokens = toFiniteNumber(usage.cacheWrite) ?? 0;
1080
+ tokens += Math.max(0, Math.round(totalTokens ??
1081
+ inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens));
1082
+ const cost = usage.cost;
1083
+ const costTotal = cost ? toFiniteNumber(cost.total) : null;
1084
+ if (costTotal !== null) {
1085
+ costUsd += Math.max(0, costTotal);
1086
+ }
1087
+ }
1088
+ const stopReason = typeof msg.stopReason === "string" ? msg.stopReason : "";
1089
+ const msgError = typeof msg.errorMessage === "string" && msg.errorMessage.trim().length > 0
1090
+ ? msg.errorMessage.trim()
1091
+ : null;
1092
+ if (stopReason === "error" || msgError) {
1093
+ hadError = true;
1094
+ errorMessage = msgError ?? errorMessage;
1095
+ }
1096
+ }
1097
+ catch {
1098
+ // Ignore malformed lines.
1099
+ }
1100
+ }
1101
+ return {
1102
+ tokens,
1103
+ costUsd: Math.round(costUsd * 10_000) / 10_000,
1104
+ hadError,
1105
+ errorMessage,
1106
+ };
1107
+ }
1108
+ catch {
1109
+ return { tokens: 0, costUsd: 0, hadError: false, errorMessage: null };
1110
+ }
1111
+ }
1112
+ async function reconcileStoppedAgentRuns() {
1113
+ try {
1114
+ const store = readAgentRuns();
1115
+ const runs = Object.values(store.runs ?? {});
1116
+ for (const run of runs) {
1117
+ if (!run || typeof run !== "object")
1118
+ continue;
1119
+ if (run.status !== "running")
1120
+ continue;
1121
+ if (!run.pid || isPidAlive(run.pid))
1122
+ continue;
1123
+ const stopped = markAgentRunStopped(run.runId);
1124
+ if (!stopped)
1125
+ continue;
1126
+ const initiativeId = stopped.initiativeId?.trim() ?? "";
1127
+ if (!initiativeId)
1128
+ continue;
1129
+ const summary = readOpenClawSessionSummary({
1130
+ agentId: stopped.agentId,
1131
+ sessionId: stopped.runId,
1132
+ });
1133
+ const completedAt = stopped.stoppedAt ?? new Date().toISOString();
1134
+ const success = !summary.hadError;
1135
+ const correlationId = stopped.runId;
1136
+ const outcomePayload = {
1137
+ initiative_id: initiativeId,
1138
+ correlation_id: correlationId,
1139
+ source_client: "openclaw",
1140
+ execution_id: `openclaw:${stopped.runId}`,
1141
+ execution_type: "openclaw.session",
1142
+ agent_id: stopped.agentId,
1143
+ task_type: stopped.taskId ?? undefined,
1144
+ started_at: stopped.startedAt,
1145
+ completed_at: completedAt,
1146
+ inputs: {
1147
+ message: stopped.message,
1148
+ workstream_id: stopped.workstreamId,
1149
+ task_id: stopped.taskId,
1150
+ },
1151
+ outputs: {
1152
+ had_error: summary.hadError,
1153
+ error_message: summary.errorMessage,
1154
+ },
1155
+ steps: [],
1156
+ success,
1157
+ human_interventions: 0,
1158
+ errors: summary.errorMessage ? [summary.errorMessage] : [],
1159
+ metadata: {
1160
+ provider: stopped.provider,
1161
+ model: stopped.model,
1162
+ tokens: summary.tokens,
1163
+ cost_usd: summary.costUsd,
1164
+ source: "openclaw_agent_run_reconcile",
1165
+ },
1166
+ };
1167
+ const retroEntityType = stopped.taskId
1168
+ ? "task"
1169
+ : "initiative";
1170
+ const retroEntityId = stopped.taskId ?? initiativeId;
1171
+ const retroSummary = stopped.taskId
1172
+ ? `OpenClaw ${success ? "completed" : "blocked"} task ${stopped.taskId}.`
1173
+ : `OpenClaw run ${success ? "completed" : "blocked"} (session ${stopped.runId}).`;
1174
+ const retroPayload = {
1175
+ initiative_id: initiativeId,
1176
+ correlation_id: correlationId,
1177
+ source_client: "openclaw",
1178
+ entity_type: retroEntityType,
1179
+ entity_id: retroEntityId,
1180
+ title: stopped.taskId ?? stopped.runId,
1181
+ idempotency_key: `retro:${stopped.runId}`,
1182
+ retro: {
1183
+ summary: retroSummary,
1184
+ what_went_well: success ? ["Completed without runtime error."] : [],
1185
+ what_went_wrong: success
1186
+ ? []
1187
+ : [summary.errorMessage ?? "Session ended with error."],
1188
+ decisions: [],
1189
+ follow_ups: success
1190
+ ? []
1191
+ : [
1192
+ {
1193
+ title: "Investigate OpenClaw session failure and unblock task",
1194
+ priority: "p0",
1195
+ reason: summary.errorMessage ?? "Session ended with error.",
1196
+ },
1197
+ ],
1198
+ signals: {
1199
+ tokens: summary.tokens,
1200
+ cost_usd: summary.costUsd,
1201
+ had_error: summary.hadError,
1202
+ error_message: summary.errorMessage,
1203
+ session_id: stopped.runId,
1204
+ task_id: stopped.taskId,
1205
+ workstream_id: stopped.workstreamId,
1206
+ provider: stopped.provider,
1207
+ model: stopped.model,
1208
+ source: "openclaw_agent_run_reconcile",
1209
+ },
1210
+ },
1211
+ };
1212
+ try {
1213
+ await client.recordRunOutcome(outcomePayload);
1214
+ }
1215
+ catch (err) {
1216
+ const timestamp = new Date().toISOString();
1217
+ const activityItem = {
1218
+ id: randomUUID(),
1219
+ type: "run_completed",
1220
+ title: `Buffered outcome for session ${stopped.runId}`,
1221
+ description: null,
1222
+ agentId: stopped.agentId,
1223
+ agentName: null,
1224
+ requesterAgentId: stopped.agentId ?? null,
1225
+ requesterAgentName: null,
1226
+ executorAgentId: stopped.agentId ?? null,
1227
+ executorAgentName: null,
1228
+ runId: stopped.runId,
1229
+ initiativeId,
1230
+ timestamp,
1231
+ phase: success ? "completed" : "blocked",
1232
+ summary: retroSummary,
1233
+ metadata: {
1234
+ source: "openclaw_local_fallback",
1235
+ error: toErrorMessage(err),
1236
+ },
1237
+ };
1238
+ await appendToOutbox(initiativeId, {
1239
+ id: randomUUID(),
1240
+ type: "outcome",
1241
+ timestamp,
1242
+ payload: outcomePayload,
1243
+ activityItem,
1244
+ });
1245
+ }
1246
+ try {
1247
+ await client.recordRunRetro(retroPayload);
1248
+ }
1249
+ catch (err) {
1250
+ const timestamp = new Date().toISOString();
1251
+ const activityItem = {
1252
+ id: randomUUID(),
1253
+ type: "artifact_created",
1254
+ title: `Buffered retro for session ${stopped.runId}`,
1255
+ description: null,
1256
+ agentId: stopped.agentId,
1257
+ agentName: null,
1258
+ requesterAgentId: stopped.agentId ?? null,
1259
+ requesterAgentName: null,
1260
+ executorAgentId: stopped.agentId ?? null,
1261
+ executorAgentName: null,
1262
+ runId: stopped.runId,
1263
+ initiativeId,
1264
+ timestamp,
1265
+ phase: success ? "completed" : "blocked",
1266
+ summary: retroSummary,
1267
+ metadata: {
1268
+ source: "openclaw_local_fallback",
1269
+ error: toErrorMessage(err),
1270
+ },
1271
+ };
1272
+ await appendToOutbox(initiativeId, {
1273
+ id: randomUUID(),
1274
+ type: "retro",
1275
+ timestamp,
1276
+ payload: retroPayload,
1277
+ activityItem,
1278
+ });
1279
+ }
1280
+ }
1281
+ }
1282
+ catch {
1283
+ // best effort
1284
+ }
1285
+ }
819
1286
  async function replayOutboxEvent(event) {
820
1287
  const payload = event.payload ?? {};
1288
+ function normalizeRunFields(context) {
1289
+ // We prefer correlation IDs for replay because many local adapters use UUID-like
1290
+ // session IDs that do *not* exist as server-side run IDs.
1291
+ if (context.correlationId) {
1292
+ return { run_id: undefined, correlation_id: context.correlationId };
1293
+ }
1294
+ if (context.runId) {
1295
+ return {
1296
+ run_id: undefined,
1297
+ correlation_id: `openclaw_run_${stableHash(context.runId).slice(0, 24)}`,
1298
+ };
1299
+ }
1300
+ return { run_id: undefined, correlation_id: undefined };
1301
+ }
821
1302
  if (event.type === "progress") {
822
1303
  const message = extractProgressOutboxMessage(payload);
823
1304
  if (!message) {
@@ -848,7 +1329,12 @@ export default function register(api) {
848
1329
  const meta = metaRaw && typeof metaRaw === "object" && !Array.isArray(metaRaw)
849
1330
  ? metaRaw
850
1331
  : {};
851
- const emitPayload = {
1332
+ const baseMetadata = {
1333
+ ...meta,
1334
+ source: "orgx_openclaw_outbox_replay",
1335
+ outbox_event_id: event.id,
1336
+ };
1337
+ let emitPayload = {
852
1338
  initiative_id: context.value.initiativeId,
853
1339
  run_id: context.value.runId,
854
1340
  correlation_id: context.value.correlationId,
@@ -860,12 +1346,22 @@ export default function register(api) {
860
1346
  next_step: pickStringField(payload, "next_step") ??
861
1347
  pickStringField(payload, "nextStep") ??
862
1348
  undefined,
863
- metadata: {
864
- ...meta,
865
- source: "orgx_openclaw_outbox_replay",
866
- outbox_event_id: event.id,
867
- },
1349
+ metadata: baseMetadata,
868
1350
  };
1351
+ // Locally-buffered progress events often store a local UUID in run_id. OrgX may reject
1352
+ // unknown run IDs on replay; prefer a deterministic non-UUID correlation key instead.
1353
+ if (emitPayload.run_id && !emitPayload.correlation_id) {
1354
+ const replayCorrelationId = `openclaw_run_${stableHash(emitPayload.run_id).slice(0, 24)}`;
1355
+ emitPayload = {
1356
+ ...emitPayload,
1357
+ run_id: undefined,
1358
+ correlation_id: replayCorrelationId,
1359
+ metadata: {
1360
+ ...(emitPayload.metadata ?? {}),
1361
+ replay_run_id_as_correlation: true,
1362
+ },
1363
+ };
1364
+ }
869
1365
  try {
870
1366
  await client.emitActivity(emitPayload);
871
1367
  }
@@ -879,14 +1375,13 @@ export default function register(api) {
879
1375
  /^404\\b/.test(msg) &&
880
1376
  /\\brun\\b/i.test(msg) &&
881
1377
  /not found/i.test(msg)) {
1378
+ const replayCorrelationId = `openclaw_run_${stableHash(emitPayload.run_id).slice(0, 24)}`;
882
1379
  await client.emitActivity({
883
1380
  ...emitPayload,
884
1381
  run_id: undefined,
885
- // Avoid passing a bare UUID as correlation_id since some server
886
- // paths may interpret UUIDs as run_id lookups.
887
- correlation_id: `openclaw:${emitPayload.run_id}`,
1382
+ correlation_id: replayCorrelationId,
888
1383
  metadata: {
889
- ...emitPayload.metadata,
1384
+ ...(emitPayload.metadata ?? {}),
890
1385
  replay_run_id_as_correlation: true,
891
1386
  },
892
1387
  });
@@ -909,12 +1404,29 @@ export default function register(api) {
909
1404
  if (!context.ok) {
910
1405
  throw new Error(context.error);
911
1406
  }
1407
+ const runFields = normalizeRunFields({
1408
+ runId: context.value.runId,
1409
+ correlationId: context.value.correlationId,
1410
+ });
1411
+ // Payloads should include a stable idempotency_key when enqueued, but older
1412
+ // events may not. Derive a deterministic fallback so outbox replay won't
1413
+ // double-create the same remote decision.
1414
+ const fallbackKey = stableHash(JSON.stringify({
1415
+ t: "decision",
1416
+ initiative_id: context.value.initiativeId,
1417
+ run_id: context.value.runId ?? null,
1418
+ correlation_id: context.value.correlationId ?? null,
1419
+ question,
1420
+ })).slice(0, 24);
1421
+ const resolvedIdempotencyKey = pickStringField(payload, "idempotency_key") ??
1422
+ pickStringField(payload, "idempotencyKey") ??
1423
+ `openclaw:decision:${fallbackKey}`;
912
1424
  await client.applyChangeset({
913
1425
  initiative_id: context.value.initiativeId,
914
- run_id: context.value.runId,
915
- correlation_id: context.value.correlationId,
1426
+ run_id: runFields.run_id,
1427
+ correlation_id: runFields.correlation_id,
916
1428
  source_client: context.value.sourceClient,
917
- idempotency_key: pickStringField(payload, "idempotency_key") ?? `decision:${event.id}`,
1429
+ idempotency_key: resolvedIdempotencyKey,
918
1430
  operations: [
919
1431
  {
920
1432
  op: "decision.create",
@@ -933,6 +1445,10 @@ export default function register(api) {
933
1445
  if (!context.ok) {
934
1446
  throw new Error(context.error);
935
1447
  }
1448
+ const runFields = normalizeRunFields({
1449
+ runId: context.value.runId,
1450
+ correlationId: context.value.correlationId,
1451
+ });
936
1452
  const operations = Array.isArray(payload.operations)
937
1453
  ? payload.operations
938
1454
  : [];
@@ -942,31 +1458,267 @@ export default function register(api) {
942
1458
  });
943
1459
  return;
944
1460
  }
1461
+ // Status updates are the most common offline replay payload, and `updateEntity`
1462
+ // is the most widely supported primitive across OrgX deployments. Prefer it
1463
+ // when the changeset contains only simple status mutations.
1464
+ const statusOps = operations
1465
+ .map((op) => {
1466
+ if (!op || typeof op !== "object")
1467
+ return null;
1468
+ const record = op;
1469
+ const kind = typeof record.op === "string" ? record.op.trim() : "";
1470
+ if (kind === "task.update") {
1471
+ const taskId = typeof record.task_id === "string" ? record.task_id.trim() : "";
1472
+ const statusRaw = typeof record.status === "string" ? record.status.trim() : "";
1473
+ const normalized = statusRaw.toLowerCase().replace(/\s+/g, "_");
1474
+ const status = normalized === "completed" || normalized === "complete" || normalized === "finished"
1475
+ ? "done"
1476
+ : normalized === "inprogress"
1477
+ ? "in_progress"
1478
+ : normalized;
1479
+ if (!taskId || !status)
1480
+ return null;
1481
+ return { type: "task", id: taskId, status };
1482
+ }
1483
+ if (kind === "milestone.update") {
1484
+ const milestoneId = typeof record.milestone_id === "string" ? record.milestone_id.trim() : "";
1485
+ const statusRaw = typeof record.status === "string" ? record.status.trim() : "";
1486
+ const normalized = statusRaw.toLowerCase().replace(/\s+/g, "_");
1487
+ const status = normalized === "done" || normalized === "complete" || normalized === "finished"
1488
+ ? "completed"
1489
+ : normalized === "inprogress"
1490
+ ? "in_progress"
1491
+ : normalized === "todo" || normalized === "not_started" || normalized === "pending"
1492
+ ? "planned"
1493
+ : normalized === "blocked" || normalized === "stuck"
1494
+ ? "at_risk"
1495
+ : normalized;
1496
+ if (!milestoneId || !status)
1497
+ return null;
1498
+ return { type: "milestone", id: milestoneId, status };
1499
+ }
1500
+ return null;
1501
+ })
1502
+ .filter((item) => Boolean(item));
1503
+ if (statusOps.length === operations.length) {
1504
+ for (const op of statusOps) {
1505
+ await client.updateEntity(op.type, op.id, { status: op.status });
1506
+ }
1507
+ return;
1508
+ }
1509
+ // Payloads should include a stable idempotency_key when enqueued, but older
1510
+ // events may not. Derive a deterministic fallback so outbox replay won't
1511
+ // double-create the same remote change.
1512
+ const fallbackKey = stableHash(JSON.stringify({
1513
+ t: "changeset",
1514
+ initiative_id: context.value.initiativeId,
1515
+ run_id: context.value.runId ?? null,
1516
+ correlation_id: context.value.correlationId ?? null,
1517
+ operations,
1518
+ })).slice(0, 24);
1519
+ const resolvedIdempotencyKey = pickStringField(payload, "idempotency_key") ??
1520
+ pickStringField(payload, "idempotencyKey") ??
1521
+ `openclaw:changeset:${fallbackKey}`;
945
1522
  await client.applyChangeset({
946
1523
  initiative_id: context.value.initiativeId,
947
- run_id: context.value.runId,
948
- correlation_id: context.value.correlationId,
1524
+ run_id: runFields.run_id,
1525
+ correlation_id: runFields.correlation_id,
949
1526
  source_client: context.value.sourceClient,
950
- idempotency_key: pickStringField(payload, "idempotency_key") ?? `changeset:${event.id}`,
1527
+ idempotency_key: resolvedIdempotencyKey,
951
1528
  operations,
952
1529
  });
953
1530
  return;
954
1531
  }
1532
+ if (event.type === "outcome") {
1533
+ const context = resolveReportingContext(payload);
1534
+ if (!context.ok) {
1535
+ throw new Error(context.error);
1536
+ }
1537
+ const runFields = normalizeRunFields({
1538
+ runId: context.value.runId,
1539
+ correlationId: context.value.correlationId,
1540
+ });
1541
+ const executionId = pickStringField(payload, "execution_id") ??
1542
+ pickStringField(payload, "executionId");
1543
+ const executionType = pickStringField(payload, "execution_type") ??
1544
+ pickStringField(payload, "executionType");
1545
+ const agentId = pickStringField(payload, "agent_id") ??
1546
+ pickStringField(payload, "agentId");
1547
+ const success = typeof payload.success === "boolean"
1548
+ ? payload.success
1549
+ : null;
1550
+ if (!executionId || !executionType || !agentId || success === null) {
1551
+ api.log?.warn?.("[orgx] Dropping invalid outcome outbox event", {
1552
+ eventId: event.id,
1553
+ });
1554
+ return;
1555
+ }
1556
+ const metaRaw = payload.metadata;
1557
+ const meta = metaRaw && typeof metaRaw === "object" && !Array.isArray(metaRaw)
1558
+ ? metaRaw
1559
+ : {};
1560
+ await client.recordRunOutcome({
1561
+ initiative_id: context.value.initiativeId,
1562
+ run_id: runFields.run_id,
1563
+ correlation_id: runFields.correlation_id,
1564
+ source_client: context.value.sourceClient,
1565
+ execution_id: executionId,
1566
+ execution_type: executionType,
1567
+ agent_id: agentId,
1568
+ task_type: pickStringField(payload, "task_type") ??
1569
+ pickStringField(payload, "taskType") ??
1570
+ undefined,
1571
+ domain: pickStringField(payload, "domain") ?? undefined,
1572
+ started_at: pickStringField(payload, "started_at") ??
1573
+ pickStringField(payload, "startedAt") ??
1574
+ undefined,
1575
+ completed_at: pickStringField(payload, "completed_at") ??
1576
+ pickStringField(payload, "completedAt") ??
1577
+ undefined,
1578
+ inputs: payload.inputs && typeof payload.inputs === "object"
1579
+ ? payload.inputs
1580
+ : undefined,
1581
+ outputs: payload.outputs && typeof payload.outputs === "object"
1582
+ ? payload.outputs
1583
+ : undefined,
1584
+ steps: Array.isArray(payload.steps)
1585
+ ? payload.steps
1586
+ : undefined,
1587
+ success,
1588
+ quality_score: typeof payload.quality_score === "number"
1589
+ ? payload.quality_score
1590
+ : typeof payload.qualityScore === "number"
1591
+ ? payload.qualityScore
1592
+ : undefined,
1593
+ duration_vs_estimate: typeof payload.duration_vs_estimate === "number"
1594
+ ? payload.duration_vs_estimate
1595
+ : typeof payload.durationVsEstimate === "number"
1596
+ ? payload.durationVsEstimate
1597
+ : undefined,
1598
+ cost_vs_budget: typeof payload.cost_vs_budget === "number"
1599
+ ? payload.cost_vs_budget
1600
+ : typeof payload.costVsBudget === "number"
1601
+ ? payload.costVsBudget
1602
+ : undefined,
1603
+ human_interventions: typeof payload.human_interventions === "number"
1604
+ ? payload.human_interventions
1605
+ : typeof payload.humanInterventions === "number"
1606
+ ? payload.humanInterventions
1607
+ : undefined,
1608
+ user_satisfaction: typeof payload.user_satisfaction === "number"
1609
+ ? payload.user_satisfaction
1610
+ : typeof payload.userSatisfaction === "number"
1611
+ ? payload.userSatisfaction
1612
+ : undefined,
1613
+ errors: Array.isArray(payload.errors)
1614
+ ? payload.errors.filter((e) => typeof e === "string")
1615
+ : undefined,
1616
+ metadata: {
1617
+ ...meta,
1618
+ source: "orgx_openclaw_outbox_replay",
1619
+ outbox_event_id: event.id,
1620
+ },
1621
+ });
1622
+ return;
1623
+ }
1624
+ if (event.type === "retro") {
1625
+ const context = resolveReportingContext(payload);
1626
+ if (!context.ok) {
1627
+ throw new Error(context.error);
1628
+ }
1629
+ const runFields = normalizeRunFields({
1630
+ runId: context.value.runId,
1631
+ correlationId: context.value.correlationId,
1632
+ });
1633
+ const retro = payload.retro && typeof payload.retro === "object" && !Array.isArray(payload.retro)
1634
+ ? payload.retro
1635
+ : null;
1636
+ const summary = retro && typeof retro.summary === "string" ? retro.summary.trim() : "";
1637
+ if (!retro || !summary) {
1638
+ api.log?.warn?.("[orgx] Dropping invalid retro outbox event", {
1639
+ eventId: event.id,
1640
+ });
1641
+ return;
1642
+ }
1643
+ const entityTypeRaw = pickStringField(payload, "entity_type") ??
1644
+ pickStringField(payload, "entityType");
1645
+ const parsedEntityType = parseRetroEntityType(entityTypeRaw) ?? null;
1646
+ // Server-side enum parity can lag behind local clients. Only attach to the
1647
+ // entity types that are guaranteed to exist today.
1648
+ const entityType = parsedEntityType === "initiative" || parsedEntityType === "task"
1649
+ ? parsedEntityType
1650
+ : null;
1651
+ const entityIdRaw = pickStringField(payload, "entity_id") ??
1652
+ pickStringField(payload, "entityId") ??
1653
+ null;
1654
+ const entityId = isUuid(entityIdRaw ?? undefined) ? entityIdRaw : null;
1655
+ await client.recordRunRetro({
1656
+ initiative_id: context.value.initiativeId,
1657
+ run_id: runFields.run_id,
1658
+ correlation_id: runFields.correlation_id,
1659
+ source_client: context.value.sourceClient,
1660
+ entity_type: entityType && entityId ? entityType : undefined,
1661
+ entity_id: entityType && entityId ? entityId : undefined,
1662
+ title: pickStringField(payload, "title") ?? undefined,
1663
+ idempotency_key: pickStringField(payload, "idempotency_key") ??
1664
+ pickStringField(payload, "idempotencyKey") ??
1665
+ undefined,
1666
+ retro: retro,
1667
+ markdown: pickStringField(payload, "markdown") ?? undefined,
1668
+ });
1669
+ return;
1670
+ }
955
1671
  if (event.type === "artifact") {
956
- const name = pickStringField(payload, "name");
957
- if (!name) {
1672
+ // Artifacts are first-class UX loop closure (activity stream + entity modals).
1673
+ // Try to persist upstream; if this fails, keep the event queued for retry.
1674
+ const payload = event.payload && typeof event.payload === "object" && !Array.isArray(event.payload)
1675
+ ? event.payload
1676
+ : {};
1677
+ const name = pickStringField(payload, "name") ?? pickStringField(payload, "title") ?? "";
1678
+ const artifactType = pickStringField(payload, "artifact_type") ?? "other";
1679
+ const entityType = pickStringField(payload, "entity_type") ?? "";
1680
+ const entityId = pickStringField(payload, "entity_id") ?? "";
1681
+ const artifactId = pickStringField(payload, "artifact_id") ?? null;
1682
+ const description = pickStringField(payload, "description") ?? undefined;
1683
+ const externalUrl = pickStringField(payload, "url") ?? pickStringField(payload, "artifact_url") ?? null;
1684
+ const content = pickStringField(payload, "content") ?? pickStringField(payload, "preview_markdown") ?? null;
1685
+ const allowedEntityType = entityType === "initiative" ||
1686
+ entityType === "milestone" ||
1687
+ entityType === "task" ||
1688
+ entityType === "decision" ||
1689
+ entityType === "project"
1690
+ ? entityType
1691
+ : null;
1692
+ if (!allowedEntityType || !entityId.trim() || !name.trim()) {
958
1693
  api.log?.warn?.("[orgx] Dropping invalid artifact outbox event", {
959
1694
  eventId: event.id,
1695
+ entityType,
1696
+ entityId,
960
1697
  });
961
1698
  return;
962
1699
  }
963
- await client.createEntity("artifact", {
964
- name,
965
- artifact_type: pickStringField(payload, "artifact_type") ?? "other",
966
- description: pickStringField(payload, "description"),
967
- artifact_url: pickStringField(payload, "url"),
968
- status: "active",
1700
+ const result = await registerArtifact(client, client.getBaseUrl(), {
1701
+ artifact_id: artifactId,
1702
+ entity_type: allowedEntityType,
1703
+ entity_id: entityId,
1704
+ name: name.trim(),
1705
+ artifact_type: artifactType.trim() || "other",
1706
+ description,
1707
+ external_url: externalUrl,
1708
+ preview_markdown: content,
1709
+ status: "draft",
1710
+ metadata: {
1711
+ source: "outbox_replay",
1712
+ outbox_event_id: event.id,
1713
+ ...(payload.metadata && typeof payload.metadata === "object" && !Array.isArray(payload.metadata)
1714
+ ? payload.metadata
1715
+ : {}),
1716
+ },
1717
+ validate_persistence: process.env.ORGX_VALIDATE_ARTIFACT_PERSISTENCE === "1",
969
1718
  });
1719
+ if (!result.ok) {
1720
+ throw new Error(result.persistence.last_error ?? "artifact registration failed");
1721
+ }
970
1722
  return;
971
1723
  }
972
1724
  }
@@ -1040,6 +1792,9 @@ export default function register(api) {
1040
1792
  return syncInFlight;
1041
1793
  }
1042
1794
  syncInFlight = (async () => {
1795
+ if (!config.apiKey) {
1796
+ refreshConfigFromSources({ reason: "sync_no_api_key" });
1797
+ }
1043
1798
  if (!config.apiKey) {
1044
1799
  updateOnboardingState({
1045
1800
  status: "idle",
@@ -1050,25 +1805,123 @@ export default function register(api) {
1050
1805
  return;
1051
1806
  }
1052
1807
  try {
1053
- updateCachedSnapshot(await client.getOrgSnapshot());
1808
+ await reconcileStoppedAgentRuns();
1809
+ let snapshotError = null;
1810
+ try {
1811
+ updateCachedSnapshot(await client.getOrgSnapshot());
1812
+ }
1813
+ catch (err) {
1814
+ if (isAuthFailure(err)) {
1815
+ throw err;
1816
+ }
1817
+ snapshotError = toErrorMessage(err);
1818
+ api.log?.warn?.("[orgx] Snapshot sync failed (continuing)", {
1819
+ error: snapshotError,
1820
+ });
1821
+ }
1822
+ // Best-effort: poll the canonical OrgX SkillPack so the dashboard/install path
1823
+ // can apply it without blocking on an on-demand fetch.
1824
+ try {
1825
+ const refreshed = await refreshSkillPackState({
1826
+ getSkillPack: (args) => client.getSkillPack(args),
1827
+ });
1828
+ if (refreshed.changed) {
1829
+ void posthogCapture({
1830
+ event: "openclaw_skill_pack_updated",
1831
+ distinctId: config.installationId,
1832
+ properties: {
1833
+ plugin_version: config.pluginVersion,
1834
+ skill_pack_name: refreshed.state.pack?.name ?? null,
1835
+ skill_pack_version: refreshed.state.pack?.version ?? null,
1836
+ skill_pack_checksum: refreshed.state.pack?.checksum ?? null,
1837
+ },
1838
+ }).catch(() => {
1839
+ // best effort
1840
+ });
1841
+ }
1842
+ }
1843
+ catch {
1844
+ // best effort
1845
+ }
1846
+ // Best-effort: provision/update the OrgX agent suite after we've verified a working connection.
1847
+ // This makes domain agents available immediately for launches without requiring a manual install.
1848
+ try {
1849
+ if (config.autoInstallAgentSuiteOnConnect !== false) {
1850
+ const state = readSkillPackState();
1851
+ const updateAvailable = Boolean(state.remote?.checksum &&
1852
+ state.pack?.checksum &&
1853
+ state.remote.checksum !== state.pack.checksum);
1854
+ const plan = computeOrgxAgentSuitePlan({
1855
+ packVersion: config.pluginVersion || "0.0.0",
1856
+ skillPack: state.overrides,
1857
+ skillPackRemote: state.remote,
1858
+ skillPackPolicy: state.policy,
1859
+ skillPackUpdateAvailable: updateAvailable,
1860
+ });
1861
+ const hasConflicts = (plan.workspaceFiles ?? []).some((f) => f.action === "conflict");
1862
+ const hasWork = Boolean(plan.openclawConfigWouldUpdate) ||
1863
+ (plan.workspaceFiles ?? []).some((f) => f.action !== "noop");
1864
+ if (hasWork && !hasConflicts) {
1865
+ const applied = applyOrgxAgentSuitePlan({
1866
+ plan,
1867
+ dryRun: false,
1868
+ skillPack: state.overrides,
1869
+ });
1870
+ void applied;
1871
+ void posthogCapture({
1872
+ event: "openclaw_agent_suite_auto_install",
1873
+ distinctId: config.installationId,
1874
+ properties: {
1875
+ plugin_version: (config.pluginVersion ?? "").trim() || null,
1876
+ skill_pack_source: plan.skillPack?.source ?? null,
1877
+ skill_pack_checksum: plan.skillPack?.checksum ?? null,
1878
+ skill_pack_version: plan.skillPack?.version ?? null,
1879
+ openclaw_config_updated: Boolean(plan.openclawConfigWouldUpdate),
1880
+ },
1881
+ }).catch(() => {
1882
+ // best effort
1883
+ });
1884
+ }
1885
+ }
1886
+ }
1887
+ catch (err) {
1888
+ api.log?.debug?.("[orgx] Agent suite auto-provision skipped/failed (best effort)", {
1889
+ error: err instanceof Error ? err.message : String(err),
1890
+ });
1891
+ }
1054
1892
  updateOnboardingState({
1055
1893
  status: "connected",
1056
1894
  hasApiKey: true,
1057
- connectionVerified: true,
1058
- lastError: null,
1895
+ connectionVerified: snapshotError === null,
1896
+ lastError: snapshotError,
1059
1897
  nextAction: "open_dashboard",
1060
1898
  });
1061
1899
  await flushOutboxQueues();
1062
1900
  api.log?.debug?.("[orgx] Sync OK");
1063
1901
  }
1064
1902
  catch (err) {
1903
+ const authFailure = isAuthFailure(err);
1904
+ const errorMessage = authFailure
1905
+ ? "Unauthorized. Your OrgX key may be revoked or expired. Reconnect in browser or use API key."
1906
+ : toErrorMessage(err);
1065
1907
  updateOnboardingState({
1066
1908
  status: "error",
1067
1909
  hasApiKey: true,
1068
1910
  connectionVerified: false,
1069
- lastError: toErrorMessage(err),
1911
+ lastError: errorMessage,
1070
1912
  nextAction: "reconnect",
1071
1913
  });
1914
+ if (authFailure) {
1915
+ void posthogCapture({
1916
+ event: "openclaw_sync_auth_failed",
1917
+ distinctId: config.installationId,
1918
+ properties: {
1919
+ plugin_version: config.pluginVersion,
1920
+ },
1921
+ }).catch(() => {
1922
+ // best effort
1923
+ });
1924
+ }
1072
1925
  api.log?.warn?.(`[orgx] Sync failed: ${err instanceof Error ? err.message : err}`);
1073
1926
  }
1074
1927
  })();
@@ -1100,7 +1953,10 @@ export default function register(api) {
1100
1953
  openclawVersion: input.openclawVersion,
1101
1954
  platform: input.platform || process.platform,
1102
1955
  deviceName: input.deviceName,
1103
- });
1956
+ },
1957
+ // Pairing can hit a cold serverless boot + supabase insert + rate-limit checks.
1958
+ // Give it more headroom than typical lightweight API calls.
1959
+ { timeoutMs: 30_000 });
1104
1960
  if (!started.ok) {
1105
1961
  if (isAuthRequiredError(started)) {
1106
1962
  clearPairingState();
@@ -1124,7 +1980,8 @@ export default function register(api) {
1124
1980
  state,
1125
1981
  };
1126
1982
  }
1127
- const message = `Pairing start failed: ${started.error}`;
1983
+ const statusLabel = started.status ? ` (HTTP ${started.status})` : "";
1984
+ const message = `Pairing start failed${statusLabel}: ${started.error}`;
1128
1985
  updateOnboardingState({
1129
1986
  status: "error",
1130
1987
  hasApiKey: Boolean(config.apiKey),
@@ -1196,9 +2053,15 @@ export default function register(api) {
1196
2053
  nextAction: "retry",
1197
2054
  });
1198
2055
  }
2056
+ const pairingUserIdRaw = typeof polled.data.supabaseUserId === "string"
2057
+ ? polled.data.supabaseUserId
2058
+ : typeof polled.data.userId === "string"
2059
+ ? polled.data.userId
2060
+ : null;
1199
2061
  setRuntimeApiKey({
1200
2062
  apiKey: key,
1201
2063
  source: "browser_pairing",
2064
+ userId: resolveRuntimeUserId(key, [pairingUserIdRaw, config.userId]) || null,
1202
2065
  workspaceName: polled.data.workspaceName ?? null,
1203
2066
  keyPrefix: polled.data.keyPrefix ?? null,
1204
2067
  });
@@ -1875,6 +2738,48 @@ export default function register(api) {
1875
2738
  }
1876
2739
  },
1877
2740
  }, { optional: true });
2741
+ function withProvenanceMetadata(metadata) {
2742
+ const input = metadata ?? {};
2743
+ const out = { ...input };
2744
+ if (out.orgx_plugin_version === undefined) {
2745
+ out.orgx_plugin_version = (config.pluginVersion ?? "").trim() || null;
2746
+ }
2747
+ try {
2748
+ const state = readSkillPackState();
2749
+ const overrides = state.overrides;
2750
+ if (out.skill_pack_name === undefined) {
2751
+ out.skill_pack_name = overrides?.name ?? state.pack?.name ?? null;
2752
+ }
2753
+ if (out.skill_pack_version === undefined) {
2754
+ out.skill_pack_version = overrides?.version ?? state.pack?.version ?? null;
2755
+ }
2756
+ if (out.skill_pack_checksum === undefined) {
2757
+ out.skill_pack_checksum = overrides?.checksum ?? state.pack?.checksum ?? null;
2758
+ }
2759
+ if (out.skill_pack_source === undefined) {
2760
+ out.skill_pack_source = overrides?.source ?? null;
2761
+ }
2762
+ if (out.skill_pack_etag === undefined) {
2763
+ out.skill_pack_etag = state.etag ?? null;
2764
+ }
2765
+ }
2766
+ catch {
2767
+ // best effort
2768
+ }
2769
+ if (out.orgx_provenance === undefined) {
2770
+ out.orgx_provenance = {
2771
+ plugin_version: out.orgx_plugin_version ?? null,
2772
+ skill_pack: {
2773
+ name: out.skill_pack_name ?? null,
2774
+ version: out.skill_pack_version ?? null,
2775
+ checksum: out.skill_pack_checksum ?? null,
2776
+ source: out.skill_pack_source ?? null,
2777
+ etag: out.skill_pack_etag ?? null,
2778
+ },
2779
+ };
2780
+ }
2781
+ return out;
2782
+ }
1878
2783
  async function emitActivityWithFallback(source, payload) {
1879
2784
  if (!payload.message || payload.message.trim().length === 0) {
1880
2785
  return text("❌ message is required");
@@ -1895,10 +2800,10 @@ export default function register(api) {
1895
2800
  progress_pct: payload.progress_pct,
1896
2801
  level: payload.level ?? "info",
1897
2802
  next_step: payload.next_step,
1898
- metadata: {
2803
+ metadata: withProvenanceMetadata({
1899
2804
  ...(payload.metadata ?? {}),
1900
2805
  source,
1901
- },
2806
+ }),
1902
2807
  };
1903
2808
  const activityItem = {
1904
2809
  id,
@@ -1907,6 +2812,10 @@ export default function register(api) {
1907
2812
  description: payload.next_step ?? null,
1908
2813
  agentId: null,
1909
2814
  agentName: null,
2815
+ requesterAgentId: null,
2816
+ requesterAgentName: null,
2817
+ executorAgentId: null,
2818
+ executorAgentName: null,
1910
2819
  runId: context.value.runId ?? null,
1911
2820
  initiativeId: context.value.initiativeId,
1912
2821
  timestamp: now,
@@ -1956,15 +2865,19 @@ export default function register(api) {
1956
2865
  description: `${payload.operations.length} operation${payload.operations.length === 1 ? "" : "s"}`,
1957
2866
  agentId: null,
1958
2867
  agentName: null,
2868
+ requesterAgentId: null,
2869
+ requesterAgentName: null,
2870
+ executorAgentId: null,
2871
+ executorAgentName: null,
1959
2872
  runId: context.value.runId ?? null,
1960
2873
  initiativeId: context.value.initiativeId,
1961
2874
  timestamp: now,
1962
2875
  phase: "review",
1963
2876
  summary: `${payload.operations.length} operation${payload.operations.length === 1 ? "" : "s"}`,
1964
- metadata: {
2877
+ metadata: withProvenanceMetadata({
1965
2878
  source,
1966
2879
  idempotency_key: idempotencyKey,
1967
- },
2880
+ }),
1968
2881
  };
1969
2882
  try {
1970
2883
  const result = await client.applyChangeset(requestPayload);
@@ -2235,18 +3148,30 @@ export default function register(api) {
2235
3148
  // --- orgx_register_artifact ---
2236
3149
  registerMcpTool({
2237
3150
  name: "orgx_register_artifact",
2238
- description: "Register a work output (PR, document, config change, report, etc.) with OrgX. Makes it visible in the dashboard.",
3151
+ description: "Register a work output (PR, document, config change, report, etc.) as a work_artifact in OrgX. Makes it visible in the dashboard activity timeline and entity detail modals.",
2239
3152
  parameters: {
2240
3153
  type: "object",
2241
3154
  properties: {
3155
+ initiative_id: {
3156
+ type: "string",
3157
+ description: "Convenience: initiative UUID. Used as entity_type='initiative', entity_id=<this> when entity_type/entity_id are not provided.",
3158
+ },
3159
+ entity_type: {
3160
+ type: "string",
3161
+ enum: ["initiative", "milestone", "task", "decision", "project"],
3162
+ description: "The type of entity this artifact is attached to",
3163
+ },
3164
+ entity_id: {
3165
+ type: "string",
3166
+ description: "UUID of the entity this artifact is attached to",
3167
+ },
2242
3168
  name: {
2243
3169
  type: "string",
2244
3170
  description: "Human-readable artifact name (e.g., 'PR #107: Fix build size')",
2245
3171
  },
2246
3172
  artifact_type: {
2247
3173
  type: "string",
2248
- enum: ["pr", "commit", "document", "config", "report", "design", "other"],
2249
- description: "Type of artifact",
3174
+ description: "Artifact type code (e.g., 'eng.diff_pack', 'pr', 'document'). Falls back to 'shared.project_handbook' if the type is not recognized by OrgX.",
2250
3175
  },
2251
3176
  description: {
2252
3177
  type: "string",
@@ -2254,7 +3179,11 @@ export default function register(api) {
2254
3179
  },
2255
3180
  url: {
2256
3181
  type: "string",
2257
- description: "Link to the artifact (PR URL, file path, etc.)",
3182
+ description: "External link to the artifact (PR URL, file path, etc.)",
3183
+ },
3184
+ content: {
3185
+ type: "string",
3186
+ description: "Inline preview content (markdown/text). At least one of url or content is required.",
2258
3187
  },
2259
3188
  },
2260
3189
  required: ["name", "artifact_type"],
@@ -2263,6 +3192,32 @@ export default function register(api) {
2263
3192
  async execute(_callId, params = { name: "", artifact_type: "other" }) {
2264
3193
  const now = new Date().toISOString();
2265
3194
  const id = `artifact:${randomUUID().slice(0, 8)}`;
3195
+ // Resolve entity association: explicit entity_type+entity_id > initiative_id > inferred
3196
+ let resolvedEntityType = null;
3197
+ let resolvedEntityId = null;
3198
+ if (params.entity_type && isUuid(params.entity_id)) {
3199
+ resolvedEntityType = params.entity_type;
3200
+ resolvedEntityId = params.entity_id;
3201
+ }
3202
+ else if (isUuid(params.initiative_id)) {
3203
+ resolvedEntityType = "initiative";
3204
+ resolvedEntityId = params.initiative_id;
3205
+ }
3206
+ else {
3207
+ const inferred = inferReportingInitiativeId(params);
3208
+ if (inferred) {
3209
+ resolvedEntityType = "initiative";
3210
+ resolvedEntityId = inferred;
3211
+ }
3212
+ }
3213
+ if (!resolvedEntityType || !resolvedEntityId) {
3214
+ return text("❌ Cannot register artifact: provide entity_type + entity_id, or initiative_id, so the artifact can be attached to an entity.");
3215
+ }
3216
+ if (!params.url && !params.content) {
3217
+ return text("❌ Cannot register artifact: provide at least one of url or content.");
3218
+ }
3219
+ const baseUrl = client.getBaseUrl();
3220
+ const artifactId = randomUUID();
2266
3221
  const activityItem = {
2267
3222
  id,
2268
3223
  type: "artifact_created",
@@ -2270,32 +3225,66 @@ export default function register(api) {
2270
3225
  description: params.description ?? null,
2271
3226
  agentId: null,
2272
3227
  agentName: null,
3228
+ requesterAgentId: null,
3229
+ requesterAgentName: null,
3230
+ executorAgentId: null,
3231
+ executorAgentName: null,
2273
3232
  runId: null,
2274
- initiativeId: null,
3233
+ initiativeId: resolvedEntityType === "initiative" ? resolvedEntityId : null,
2275
3234
  timestamp: now,
2276
3235
  summary: params.url ?? null,
2277
- metadata: {
3236
+ metadata: withProvenanceMetadata({
2278
3237
  source: "orgx_register_artifact",
2279
3238
  artifact_type: params.artifact_type,
2280
3239
  url: params.url,
2281
- },
3240
+ entity_type: resolvedEntityType,
3241
+ entity_id: resolvedEntityId,
3242
+ }),
2282
3243
  };
2283
3244
  try {
2284
- const entity = await client.createEntity("artifact", {
3245
+ const result = await registerArtifact(client, baseUrl, {
3246
+ artifact_id: artifactId,
3247
+ entity_type: resolvedEntityType,
3248
+ entity_id: resolvedEntityId,
2285
3249
  name: params.name,
2286
3250
  artifact_type: params.artifact_type,
2287
- description: params.description,
2288
- artifact_url: params.url,
2289
- status: "active",
3251
+ description: params.description ?? null,
3252
+ external_url: params.url ?? null,
3253
+ preview_markdown: params.content ?? null,
3254
+ status: "draft",
3255
+ metadata: {
3256
+ source: "orgx_register_artifact",
3257
+ artifact_id: artifactId,
3258
+ },
3259
+ validate_persistence: true,
3260
+ });
3261
+ if (!result.ok) {
3262
+ throw new Error(result.persistence.last_error ?? "Artifact registration failed");
3263
+ }
3264
+ activityItem.metadata = withProvenanceMetadata({
3265
+ ...activityItem.metadata,
3266
+ artifact_id: result.artifact_id,
3267
+ entity_type: resolvedEntityType,
3268
+ entity_id: resolvedEntityId,
2290
3269
  });
2291
- return json(`Artifact registered: ${params.name} [${params.artifact_type}]`, entity);
3270
+ return json(`Artifact registered: ${params.name} [${params.artifact_type}] → ${resolvedEntityType}/${resolvedEntityId} (id: ${result.artifact_id})`, result);
2292
3271
  }
2293
- catch {
3272
+ catch (firstError) {
3273
+ // Outbox fallback for offline/error scenarios
2294
3274
  await appendToOutbox("artifacts", {
2295
3275
  id,
2296
3276
  type: "artifact",
2297
3277
  timestamp: now,
2298
- payload: params,
3278
+ payload: {
3279
+ artifact_id: artifactId,
3280
+ name: params.name,
3281
+ artifact_type: params.artifact_type,
3282
+ description: params.description,
3283
+ url: params.url,
3284
+ content: params.content,
3285
+ entity_type: resolvedEntityType,
3286
+ entity_id: resolvedEntityId,
3287
+ },
2299
3288
  activityItem,
2300
3289
  });
2301
3290
  return text(`Artifact saved locally: ${params.name} [${params.artifact_type}] (will sync when connected)`);
@@ -2406,8 +3395,29 @@ export default function register(api) {
2406
3395
  }, {
2407
3396
  getHealth: async (input = {}) => buildHealthReport({ probeRemote: input.probeRemote === true }),
2408
3397
  });
3398
+ const mcpPromptRegistry = new Map();
3399
+ mcpPromptRegistry.set("ship", {
3400
+ name: "ship",
3401
+ description: "Commit local changes, open a PR, and merge it (GitHub CLI required).",
3402
+ arguments: [],
3403
+ messages: [
3404
+ {
3405
+ role: "user",
3406
+ content: [
3407
+ "Ship the current work:",
3408
+ "- Inspect `git status -sb` and `git diff --stat` and summarize what will be shipped.",
3409
+ "- Run `npm run typecheck`, `npm run test:hooks`, and `npm run build` (fix failures).",
3410
+ "- Create a feature branch if on `main`.",
3411
+ "- Commit with a clear message (do not include secrets).",
3412
+ "- Push branch, open a PR (use `gh pr create`), then merge it (use `gh pr merge --merge --auto`).",
3413
+ "- If `gh` is not authenticated, stop and tell me what to run.",
3414
+ ].join("\n"),
3415
+ },
3416
+ ],
3417
+ });
2409
3418
  const mcpHttpHandler = createMcpHttpHandler({
2410
3419
  tools: mcpToolRegistry,
3420
+ prompts: mcpPromptRegistry,
2411
3421
  logger: api.log ?? {},
2412
3422
  serverName: "@useorgx/openclaw-plugin",
2413
3423
  serverVersion: config.pluginVersion,