@useorgx/openclaw-plugin 0.4.6 → 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.
@@ -17,7 +17,7 @@
17
17
  * /orgx/api/runs/:id/checkpoints/:checkpointId/restore → restore checkpoint
18
18
  * /orgx/api/runs/:id/actions/:action → run control action
19
19
  */
20
- import { readFileSync, existsSync, mkdirSync, chmodSync, createWriteStream, readdirSync, statSync, writeFileSync, } from "node:fs";
20
+ import { readFileSync, existsSync, mkdirSync, chmodSync, createWriteStream, readdirSync, statSync, writeFileSync, openSync, readSync, closeSync, } from "node:fs";
21
21
  import { homedir } from "node:os";
22
22
  import { join, extname, normalize, resolve, relative, sep, dirname } from "node:path";
23
23
  import { fileURLToPath } from "node:url";
@@ -25,6 +25,7 @@ import { spawn, spawnSync } from "node:child_process";
25
25
  import { createHash, randomUUID } from "node:crypto";
26
26
  import { backupCorruptFileSync, writeFileAtomicSync } from "./fs-utils.js";
27
27
  import { getOrgxPluginConfigDir } from "./paths.js";
28
+ import { registerArtifact } from "./artifacts/register-artifact.js";
28
29
  import { readNextUpQueuePins, removeNextUpQueuePin, setNextUpQueuePinOrder, upsertNextUpQueuePin, } from "./next-up-queue-store.js";
29
30
  import { formatStatus, formatAgents, formatActivity, formatInitiatives, getOnboardingState, } from "./dashboard-api.js";
30
31
  import { loadLocalOpenClawSnapshot, loadLocalTurnDetail, toLocalLiveActivity, toLocalLiveAgents, toLocalLiveInitiatives, toLocalSessionTree, } from "./local-openclaw.js";
@@ -35,6 +36,7 @@ import { readAgentContexts, upsertAgentContext, upsertRunContext } from "./agent
35
36
  import { getAgentRun, markAgentRunStopped, readAgentRuns, upsertAgentRun, } from "./agent-run-store.js";
36
37
  import { appendEntityComment, listEntityComments, mergeEntityComments, } from "./entity-comment-store.js";
37
38
  import { appendActivityItems, listActivityPage, } from "./activity-store.js";
39
+ import { enrichActivityActorFields } from "./activity-actor-fields.js";
38
40
  import { readByokKeys, writeByokKeys } from "./byok-store.js";
39
41
  import { applyOrgxAgentSuitePlan, computeOrgxAgentSuitePlan, generateAgentSuiteOperationId, } from "./agent-suite.js";
40
42
  import { computeMilestoneRollup, computeWorkstreamRollup, } from "./reporting/rollups.js";
@@ -53,11 +55,17 @@ async function resolveSkillPackOverrides(input) {
53
55
  const getSkillPack = input.client.getSkillPack;
54
56
  if (typeof getSkillPack !== "function")
55
57
  return state.overrides;
56
- const refreshed = await refreshSkillPackState({
57
- getSkillPack: (args) => getSkillPack(args),
58
- force,
59
- });
60
- return refreshed.state.overrides;
58
+ try {
59
+ const refreshed = await refreshSkillPackState({
60
+ getSkillPack: (args) => getSkillPack(args),
61
+ force,
62
+ });
63
+ return refreshed.state.overrides;
64
+ }
65
+ catch {
66
+ // If refresh fails (network, disk, etc.), fall back to cached overrides.
67
+ return state.overrides;
68
+ }
61
69
  }
62
70
  function safeErrorMessage(err) {
63
71
  if (err instanceof Error)
@@ -96,6 +104,167 @@ function isUnauthorizedOrgxError(err) {
96
104
  }
97
105
  const ACTIVITY_WARM_THROTTLE_MS = 30_000;
98
106
  const activityWarmByKey = new Map();
107
+ const SNAPSHOT_RESPONSE_CACHE_TTL_MS = 1_500;
108
+ const SNAPSHOT_RESPONSE_CACHE_MAX_ENTRIES = 16;
109
+ const SNAPSHOT_ACTIVITY_PERSIST_MIN_INTERVAL_MS = 15_000;
110
+ const SNAPSHOT_ACTIVITY_FINGERPRINT_DEPTH = 8;
111
+ let lastSnapshotActivityPersistAt = 0;
112
+ let lastSnapshotActivityFingerprint = "";
113
+ const snapshotResponseCache = new Map();
114
+ const ACTIVITY_DECISION_EVENT_HINTS = new Set([
115
+ "decision_buffered",
116
+ "auto_continue_spawn_guard_blocked",
117
+ "autopilot_slice_mcp_handshake_failed",
118
+ "autopilot_slice_timeout",
119
+ "autopilot_slice_log_stall",
120
+ ]);
121
+ const ACTIVITY_ARTIFACT_EVENT_HINTS = new Set([
122
+ "autopilot_slice_artifact_buffered",
123
+ ]);
124
+ function normalizeActivityBucket(value) {
125
+ if (typeof value !== "string")
126
+ return null;
127
+ const normalized = value.trim().toLowerCase();
128
+ if (normalized === "artifact")
129
+ return "artifact";
130
+ if (normalized === "decision")
131
+ return "decision";
132
+ if (normalized === "message")
133
+ return "message";
134
+ return null;
135
+ }
136
+ function activityMetadataBoolean(metadata, keys) {
137
+ if (!metadata)
138
+ return null;
139
+ for (const key of keys) {
140
+ const value = metadata[key];
141
+ if (typeof value === "boolean")
142
+ return value;
143
+ if (typeof value === "string") {
144
+ const normalized = value.trim().toLowerCase();
145
+ if (normalized === "true")
146
+ return true;
147
+ if (normalized === "false")
148
+ return false;
149
+ }
150
+ }
151
+ return null;
152
+ }
153
+ function activityMetadataNumber(metadata, keys) {
154
+ if (!metadata)
155
+ return null;
156
+ for (const key of keys) {
157
+ const value = metadata[key];
158
+ if (typeof value === "number" && Number.isFinite(value)) {
159
+ return Math.max(0, value);
160
+ }
161
+ if (typeof value === "string") {
162
+ const parsed = Number(value);
163
+ if (Number.isFinite(parsed)) {
164
+ return Math.max(0, parsed);
165
+ }
166
+ }
167
+ }
168
+ return null;
169
+ }
170
+ function activityMetadataEventName(metadata) {
171
+ if (!metadata)
172
+ return null;
173
+ const raw = metadata.event;
174
+ if (typeof raw !== "string")
175
+ return null;
176
+ const normalized = raw.trim().toLowerCase();
177
+ return normalized.length > 0 ? normalized : null;
178
+ }
179
+ function deriveStructuredActivityBucket(input) {
180
+ const metadata = input.metadata;
181
+ const explicit = normalizeActivityBucket(input.explicitBucket) ??
182
+ normalizeActivityBucket(metadata?.activity_bucket) ??
183
+ normalizeActivityBucket(metadata?.activityBucket) ??
184
+ normalizeActivityBucket(metadata?.bucket) ??
185
+ null;
186
+ if (explicit)
187
+ return explicit;
188
+ const event = activityMetadataEventName(metadata);
189
+ const decisionRequired = activityMetadataBoolean(metadata, ["decision_required", "decisionRequired"]) === true;
190
+ const artifacts = activityMetadataNumber(metadata, ["artifacts", "artifact_count", "artifactCount"]) ?? 0;
191
+ const decisions = activityMetadataNumber(metadata, ["decisions", "decision_count", "decisionCount"]) ?? 0;
192
+ const blockingDecisions = activityMetadataNumber(metadata, [
193
+ "blocking_decisions",
194
+ "blockingDecisions",
195
+ "blocking_decision_count",
196
+ "blockingDecisionCount",
197
+ ]) ?? 0;
198
+ const nonBlockingDecisions = activityMetadataNumber(metadata, [
199
+ "non_blocking_decisions",
200
+ "nonBlockingDecisions",
201
+ "non_blocking_decision_count",
202
+ "nonBlockingDecisionCount",
203
+ ]) ?? 0;
204
+ if (event === "autopilot_slice_result") {
205
+ if (decisionRequired || blockingDecisions > 0)
206
+ return "decision";
207
+ if (artifacts > 0)
208
+ return "artifact";
209
+ if (decisions > 0 || nonBlockingDecisions > 0)
210
+ return "decision";
211
+ return "message";
212
+ }
213
+ if (event && ACTIVITY_ARTIFACT_EVENT_HINTS.has(event))
214
+ return "artifact";
215
+ if (event && ACTIVITY_DECISION_EVENT_HINTS.has(event))
216
+ return "decision";
217
+ const hasArtifactReference = typeof metadata?.artifact_id === "string" ||
218
+ typeof metadata?.artifactId === "string" ||
219
+ typeof metadata?.work_artifact_id === "string";
220
+ if (hasArtifactReference || artifacts > 0)
221
+ return "artifact";
222
+ if (decisionRequired || blockingDecisions > 0 || decisions > 0 || nonBlockingDecisions > 0) {
223
+ return "decision";
224
+ }
225
+ return "message";
226
+ }
227
+ function snapshotActivityFingerprint(items) {
228
+ if (!Array.isArray(items) || items.length === 0)
229
+ return "0";
230
+ const sample = items
231
+ .slice(0, SNAPSHOT_ACTIVITY_FINGERPRINT_DEPTH)
232
+ .map((item) => `${item.id}|${item.timestamp}`)
233
+ .join(";");
234
+ return `${items.length}:${sample}`;
235
+ }
236
+ function readSnapshotResponseCache(key) {
237
+ const entry = snapshotResponseCache.get(key);
238
+ if (!entry)
239
+ return null;
240
+ if (entry.expiresAt <= Date.now()) {
241
+ snapshotResponseCache.delete(key);
242
+ return null;
243
+ }
244
+ return entry.payload;
245
+ }
246
+ function writeSnapshotResponseCache(key, payload) {
247
+ const now = Date.now();
248
+ snapshotResponseCache.set(key, {
249
+ expiresAt: now + SNAPSHOT_RESPONSE_CACHE_TTL_MS,
250
+ payload,
251
+ });
252
+ if (snapshotResponseCache.size <= SNAPSHOT_RESPONSE_CACHE_MAX_ENTRIES)
253
+ return;
254
+ for (const [cachedKey, entry] of snapshotResponseCache.entries()) {
255
+ if (entry.expiresAt <= now)
256
+ snapshotResponseCache.delete(cachedKey);
257
+ }
258
+ while (snapshotResponseCache.size > SNAPSHOT_RESPONSE_CACHE_MAX_ENTRIES) {
259
+ const oldestKey = snapshotResponseCache.keys().next().value;
260
+ if (!oldestKey)
261
+ break;
262
+ snapshotResponseCache.delete(oldestKey);
263
+ }
264
+ }
265
+ function clearSnapshotResponseCache() {
266
+ snapshotResponseCache.clear();
267
+ }
99
268
  function isUserScopedApiKey(apiKey) {
100
269
  return apiKey.trim().toLowerCase().startsWith("oxk_");
101
270
  }
@@ -107,6 +276,150 @@ function parseJsonSafe(value) {
107
276
  return null;
108
277
  }
109
278
  }
279
+ function asRecord(value) {
280
+ if (!value || typeof value !== "object" || Array.isArray(value))
281
+ return null;
282
+ return value;
283
+ }
284
+ function flattenActivityMetadata(value) {
285
+ const record = asRecord(value);
286
+ if (!record)
287
+ return null;
288
+ const nested = asRecord(record.metadata);
289
+ if (!nested)
290
+ return record;
291
+ return { ...record, ...nested };
292
+ }
293
+ function metadataString(metadata, keys) {
294
+ if (!metadata)
295
+ return null;
296
+ for (const key of keys) {
297
+ const value = metadata[key];
298
+ if (typeof value === "string" && value.trim().length > 0) {
299
+ return value.trim();
300
+ }
301
+ }
302
+ return null;
303
+ }
304
+ function metadataNumber(metadata, keys) {
305
+ if (!metadata)
306
+ return null;
307
+ for (const key of keys) {
308
+ const value = metadata[key];
309
+ if (typeof value === "number" && Number.isFinite(value))
310
+ return value;
311
+ if (typeof value === "string" && value.trim().length > 0) {
312
+ const parsed = Number(value.trim());
313
+ if (Number.isFinite(parsed))
314
+ return parsed;
315
+ }
316
+ }
317
+ return null;
318
+ }
319
+ function resolveArtifactIdFromActivityItem(item) {
320
+ const metadata = flattenActivityMetadata(item.metadata);
321
+ if (!metadata)
322
+ return null;
323
+ const rawId = metadataString(metadata, ["artifact_id", "artifactId", "work_artifact_id"]);
324
+ return rawId && rawId.length > 0 ? rawId : null;
325
+ }
326
+ function resolveArtifactHref(value) {
327
+ if (!value)
328
+ return null;
329
+ const trimmed = value.trim();
330
+ if (!trimmed)
331
+ return null;
332
+ if (/^https?:\/\//i.test(trimmed))
333
+ return trimmed;
334
+ return `/orgx/api/live/filesystem/open?path=${encodeURIComponent(trimmed)}`;
335
+ }
336
+ function buildLocalArtifactDetailFallback(artifactId, warning) {
337
+ let cursor = null;
338
+ const maxPages = 8;
339
+ for (let pageIndex = 0; pageIndex < maxPages; pageIndex += 1) {
340
+ const page = listActivityPage({ limit: 500, cursor });
341
+ const activities = Array.isArray(page.activities) ? page.activities : [];
342
+ for (const item of activities) {
343
+ const matchId = resolveArtifactIdFromActivityItem(item);
344
+ if (!matchId || matchId !== artifactId)
345
+ continue;
346
+ const metadata = flattenActivityMetadata(item.metadata) ?? {};
347
+ const rawPath = metadataString(metadata, [
348
+ "url",
349
+ "path",
350
+ "file_path",
351
+ "filepath",
352
+ "artifact_path",
353
+ "output_path",
354
+ "external_url",
355
+ "artifact_url",
356
+ ]) ?? null;
357
+ const artifactHref = resolveArtifactHref(rawPath);
358
+ const artifactType = metadataString(metadata, ["artifact_type", "artifactType", "type"]) ?? "report";
359
+ const artifactName = metadataString(metadata, ["artifact_name", "artifactName", "name", "title"]) ??
360
+ item.title?.trim() ??
361
+ `Artifact ${artifactId.slice(0, 8)}`;
362
+ const status = metadataString(metadata, ["artifact_status", "status", "state"]) ??
363
+ (typeof metadata.event === "string" && metadata.event.includes("buffered")
364
+ ? "buffered"
365
+ : "draft");
366
+ const entityType = metadataString(metadata, ["entity_type", "entityType"]) ??
367
+ (metadataString(metadata, ["initiative_id", "initiativeId"]) ? "initiative" : "task");
368
+ const entityId = metadataString(metadata, [
369
+ "entity_id",
370
+ "entityId",
371
+ "task_id",
372
+ "taskId",
373
+ "workstream_id",
374
+ "workstreamId",
375
+ "initiative_id",
376
+ "initiativeId",
377
+ "run_id",
378
+ "runId",
379
+ ]) ?? "local";
380
+ const description = metadataString(metadata, ["description", "summary", "message"]) ??
381
+ item.summary ??
382
+ item.description ??
383
+ null;
384
+ const version = Math.max(1, Math.round(metadataNumber(metadata, ["version", "artifact_version"]) ?? 1));
385
+ const createdAt = item.timestamp ?? new Date().toISOString();
386
+ const updatedAt = item.timestamp ?? createdAt;
387
+ return {
388
+ artifact: {
389
+ id: artifactId,
390
+ name: artifactName,
391
+ description,
392
+ artifact_url: artifactHref ?? "",
393
+ artifact_type: artifactType,
394
+ status,
395
+ version,
396
+ entity_type: entityType,
397
+ entity_id: entityId,
398
+ metadata: {
399
+ ...metadata,
400
+ local_fallback: true,
401
+ local_warning: warning,
402
+ local_activity_event_id: item.id,
403
+ local_activity_type: item.type,
404
+ local_activity_timestamp: item.timestamp,
405
+ local_source_path: rawPath,
406
+ },
407
+ created_at: createdAt,
408
+ updated_at: updatedAt,
409
+ catalog: null,
410
+ cached_metadata: null,
411
+ },
412
+ relationships: [],
413
+ localFallback: true,
414
+ warning,
415
+ };
416
+ }
417
+ if (!page.nextCursor)
418
+ break;
419
+ cursor = page.nextCursor;
420
+ }
421
+ return null;
422
+ }
110
423
  function maskSecret(value) {
111
424
  if (!value)
112
425
  return null;
@@ -756,48 +1069,52 @@ function applyAgentContextsToActivity(input, contexts) {
756
1069
  if (!Array.isArray(input))
757
1070
  return [];
758
1071
  return input.map((item) => {
1072
+ let nextItem = item;
759
1073
  const existingInitiativeId = (item.initiativeId ?? "").trim();
760
- if (isUuidLike(existingInitiativeId))
761
- return item;
762
- const runCtx = item.runId ? contexts.runs[item.runId] : null;
763
- if (runCtx && runCtx.initiativeId && runCtx.initiativeId.trim().length > 0) {
764
- const initiativeId = runCtx.initiativeId.trim();
765
- const metadata = item.metadata && typeof item.metadata === "object"
766
- ? { ...item.metadata }
767
- : {};
768
- metadata.orgx_context = {
769
- initiativeId,
770
- workstreamId: runCtx.workstreamId ?? null,
771
- taskId: runCtx.taskId ?? null,
772
- updatedAt: runCtx.updatedAt,
773
- };
774
- return {
775
- ...item,
776
- initiativeId,
777
- metadata,
778
- };
1074
+ if (!isUuidLike(existingInitiativeId)) {
1075
+ const runCtx = item.runId ? contexts.runs[item.runId] : null;
1076
+ if (runCtx && runCtx.initiativeId && runCtx.initiativeId.trim().length > 0) {
1077
+ const initiativeId = runCtx.initiativeId.trim();
1078
+ const metadata = item.metadata && typeof item.metadata === "object"
1079
+ ? { ...item.metadata }
1080
+ : {};
1081
+ metadata.orgx_context = {
1082
+ initiativeId,
1083
+ workstreamId: runCtx.workstreamId ?? null,
1084
+ taskId: runCtx.taskId ?? null,
1085
+ updatedAt: runCtx.updatedAt,
1086
+ };
1087
+ nextItem = {
1088
+ ...item,
1089
+ initiativeId,
1090
+ metadata,
1091
+ };
1092
+ }
1093
+ else {
1094
+ const agentId = item.agentId?.trim() ?? "";
1095
+ if (agentId) {
1096
+ const ctx = contexts.agents[agentId];
1097
+ const initiativeId = ctx?.initiativeId?.trim() ?? "";
1098
+ if (initiativeId) {
1099
+ const metadata = item.metadata && typeof item.metadata === "object"
1100
+ ? { ...item.metadata }
1101
+ : {};
1102
+ metadata.orgx_context = {
1103
+ initiativeId,
1104
+ workstreamId: ctx.workstreamId ?? null,
1105
+ taskId: ctx.taskId ?? null,
1106
+ updatedAt: ctx.updatedAt,
1107
+ };
1108
+ nextItem = {
1109
+ ...item,
1110
+ initiativeId,
1111
+ metadata,
1112
+ };
1113
+ }
1114
+ }
1115
+ }
779
1116
  }
780
- const agentId = item.agentId?.trim() ?? "";
781
- if (!agentId)
782
- return item;
783
- const ctx = contexts.agents[agentId];
784
- const initiativeId = ctx?.initiativeId?.trim() ?? "";
785
- if (!initiativeId)
786
- return item;
787
- const metadata = item.metadata && typeof item.metadata === "object"
788
- ? { ...item.metadata }
789
- : {};
790
- metadata.orgx_context = {
791
- initiativeId,
792
- workstreamId: ctx.workstreamId ?? null,
793
- taskId: ctx.taskId ?? null,
794
- updatedAt: ctx.updatedAt,
795
- };
796
- return {
797
- ...item,
798
- initiativeId,
799
- metadata,
800
- };
1117
+ return enrichActivityActorFields(nextItem);
801
1118
  });
802
1119
  }
803
1120
  function mergeSessionTrees(base, extra) {
@@ -849,7 +1166,12 @@ function mergeSessionTrees(base, extra) {
849
1166
  };
850
1167
  }
851
1168
  function mergeActivities(base, extra, limit) {
852
- const merged = [...(base ?? []), ...(extra ?? [])].sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
1169
+ const merged = [...(base ?? []), ...(extra ?? [])].sort((a, b) => {
1170
+ const timestampDelta = Date.parse(b.timestamp) - Date.parse(a.timestamp);
1171
+ if (timestampDelta !== 0)
1172
+ return timestampDelta;
1173
+ return b.id.localeCompare(a.id);
1174
+ });
853
1175
  const deduped = [];
854
1176
  const seen = new Set();
855
1177
  for (const item of merged) {
@@ -899,6 +1221,56 @@ function normalizeRuntimeSource(value) {
899
1221
  return "api";
900
1222
  return "unknown";
901
1223
  }
1224
+ function runtimeSourceDefaultAgentLabel(sourceClient) {
1225
+ if (sourceClient === "codex")
1226
+ return "Codex";
1227
+ if (sourceClient === "claude-code")
1228
+ return "Claude Code";
1229
+ if (sourceClient === "openclaw")
1230
+ return "OpenClaw";
1231
+ if (sourceClient === "api")
1232
+ return "OrgX API";
1233
+ return null;
1234
+ }
1235
+ function runtimeSourceDefaultAgentId(sourceClient) {
1236
+ if (sourceClient === "codex")
1237
+ return "runtime:codex";
1238
+ if (sourceClient === "claude-code")
1239
+ return "runtime:claude-code";
1240
+ if (sourceClient === "openclaw")
1241
+ return "runtime:openclaw";
1242
+ if (sourceClient === "api")
1243
+ return "runtime:api";
1244
+ return null;
1245
+ }
1246
+ function deriveRuntimeFallbackAgent(instance) {
1247
+ const sourceClient = normalizeRuntimeSource(instance.sourceClient);
1248
+ const agentId = (instance.agentId ?? "").trim() || runtimeSourceDefaultAgentId(sourceClient);
1249
+ const agentName = (instance.agentName ?? "").trim() ||
1250
+ (instance.displayName ?? "").trim() ||
1251
+ runtimeSourceDefaultAgentLabel(sourceClient);
1252
+ return {
1253
+ agentId: agentId || null,
1254
+ agentName: agentName || null,
1255
+ };
1256
+ }
1257
+ function deriveRuntimeSessionStatus(instance) {
1258
+ const state = (instance.state ?? "").trim().toLowerCase();
1259
+ const phase = (instance.phase ?? "").trim().toLowerCase();
1260
+ if (phase === "blocked" || state === "error")
1261
+ return "blocked";
1262
+ if (phase === "completed")
1263
+ return "completed";
1264
+ if (phase === "handoff")
1265
+ return "handoff";
1266
+ if (phase === "review")
1267
+ return "review";
1268
+ if (state === "stopped")
1269
+ return "paused";
1270
+ if (state === "stale")
1271
+ return "queued";
1272
+ return "running";
1273
+ }
902
1274
  function runtimeMatchMaps(instances) {
903
1275
  const byRunId = new Map();
904
1276
  const byAgentInitiative = new Map();
@@ -931,8 +1303,32 @@ function enrichSessionsWithRuntime(input, instances) {
931
1303
  const match = byRun ?? byAgent;
932
1304
  if (!match)
933
1305
  return node;
1306
+ const runtimeStatus = deriveRuntimeSessionStatus(match);
1307
+ const fallbackAgent = deriveRuntimeFallbackAgent(match);
1308
+ const agentId = (node.agentId ?? "").trim() || fallbackAgent.agentId;
1309
+ const agentName = (node.agentName ?? "").trim() || fallbackAgent.agentName;
1310
+ const nodeStatus = (node.status ?? "").trim().toLowerCase();
1311
+ const isLiveLikeNodeStatus = nodeStatus === "running" ||
1312
+ nodeStatus === "active" ||
1313
+ nodeStatus === "in_progress" ||
1314
+ nodeStatus === "working" ||
1315
+ nodeStatus === "planning" ||
1316
+ nodeStatus === "dispatching";
1317
+ const shouldDowngradeStatusFromRuntime = isLiveLikeNodeStatus && (runtimeStatus === "queued" || runtimeStatus === "paused");
1318
+ const blockerReason = (node.blockerReason ?? "").trim() ||
1319
+ (node.status?.toLowerCase() === "blocked" || match.phase?.toLowerCase() === "blocked"
1320
+ ? (match.lastMessage ?? "").trim()
1321
+ : "");
934
1322
  return {
935
1323
  ...node,
1324
+ agentId: agentId || null,
1325
+ agentName: agentName || null,
1326
+ status: shouldDowngradeStatusFromRuntime ? runtimeStatus : node.status,
1327
+ state: node.state ?? match.state ?? null,
1328
+ lastEventSummary: shouldDowngradeStatusFromRuntime && runtimeStatus === "queued"
1329
+ ? node.lastEventSummary ?? "Recovered stale runtime; awaiting next dispatch."
1330
+ : node.lastEventSummary,
1331
+ blockerReason: blockerReason || node.blockerReason || null,
936
1332
  runtimeClient: normalizeRuntimeSource(match.sourceClient),
937
1333
  runtimeLabel: match.displayName,
938
1334
  runtimeProvider: match.providerLogo,
@@ -966,13 +1362,15 @@ function injectRuntimeInstancesAsSessions(input, instances) {
966
1362
  continue;
967
1363
  if (existingRunIds.has(runId))
968
1364
  continue;
969
- // Only surface active/stale runtime instances as "sessions" so the Activity UI can show
970
- // a seamless "in progress" lane even when this isn't an OpenClaw session tree node.
971
- if (instance.state !== "active" && instance.state !== "stale")
1365
+ // Only surface active runtime instances as synthetic sessions.
1366
+ // Stale instances are reconciled onto existing sessions but shouldn't appear as fresh work.
1367
+ if (instance.state !== "active")
972
1368
  continue;
973
1369
  const initiativeId = instance.initiativeId?.trim() || null;
974
1370
  const workstreamId = instance.workstreamId?.trim() || null;
975
- const groupId = initiativeId ?? "runtime";
1371
+ const runtimeClient = normalizeRuntimeSource(instance.sourceClient);
1372
+ const fallbackAgent = deriveRuntimeFallbackAgent(instance);
1373
+ const groupId = initiativeId ?? fallbackAgent.agentId ?? `runtime:${runtimeClient}`;
976
1374
  const meta = instance.metadata && typeof instance.metadata === "object"
977
1375
  ? instance.metadata
978
1376
  : {};
@@ -980,7 +1378,7 @@ function injectRuntimeInstancesAsSessions(input, instances) {
980
1378
  (workstreamId ? `Workstream ${workstreamId.slice(0, 8)}` : null);
981
1379
  const initiativeHint = pickString(meta, ["initiative_title", "initiativeTitle"]) ??
982
1380
  (initiativeId ? `Initiative ${initiativeId.slice(0, 8)}` : null);
983
- const groupLabel = (initiativeHint ?? groupId).trim();
1381
+ const groupLabel = (initiativeHint ?? fallbackAgent.agentName ?? groupId).trim();
984
1382
  if (!groupsById.has(groupId)) {
985
1383
  const group = { id: groupId, label: groupLabel, status: null };
986
1384
  groupsById.set(groupId, group);
@@ -991,14 +1389,19 @@ function injectRuntimeInstancesAsSessions(input, instances) {
991
1389
  continue;
992
1390
  existingNodeIds.add(nodeId);
993
1391
  existingRunIds.add(runId);
1392
+ const status = deriveRuntimeSessionStatus(instance);
1393
+ const blockerReason = status === "blocked" ? (instance.lastMessage ?? null) : null;
1394
+ const blockers = status === "blocked" && typeof blockerReason === "string" && blockerReason.trim().length > 0
1395
+ ? [blockerReason.trim()]
1396
+ : [];
994
1397
  const node = {
995
1398
  id: nodeId,
996
1399
  parentId: null,
997
1400
  runId,
998
1401
  title: titleHint ?? instance.lastMessage ?? `Runtime ${runId.slice(0, 8)}`,
999
- agentId: instance.agentId ?? null,
1000
- agentName: instance.agentName ?? null,
1001
- status: "running",
1402
+ agentId: fallbackAgent.agentId,
1403
+ agentName: fallbackAgent.agentName,
1404
+ status,
1002
1405
  progress: instance.progressPct ?? null,
1003
1406
  initiativeId,
1004
1407
  workstreamId,
@@ -1008,10 +1411,11 @@ function injectRuntimeInstancesAsSessions(input, instances) {
1008
1411
  updatedAt: instance.updatedAt ?? null,
1009
1412
  lastEventAt: instance.lastEventAt ?? null,
1010
1413
  lastEventSummary: instance.lastMessage ?? null,
1011
- blockers: [],
1414
+ blockers,
1415
+ blockerReason,
1012
1416
  phase: instance.phase ?? null,
1013
1417
  state: instance.state ?? null,
1014
- runtimeClient: normalizeRuntimeSource(instance.sourceClient),
1418
+ runtimeClient,
1015
1419
  runtimeLabel: instance.displayName,
1016
1420
  runtimeProvider: instance.providerLogo,
1017
1421
  instanceId: instance.id,
@@ -1362,6 +1766,8 @@ function resolveSafeDistPath(subPath) {
1362
1766
  // =============================================================================
1363
1767
  const IMMUTABLE_FILE_CACHE = new Map();
1364
1768
  const IMMUTABLE_FILE_CACHE_MAX = 128;
1769
+ const FILE_PREVIEW_MAX_BYTES = 1_000_000;
1770
+ const FILE_PREVIEW_MAX_DIR_ENTRIES = 300;
1365
1771
  function sendJson(res, status, data) {
1366
1772
  const body = JSON.stringify(data);
1367
1773
  res.writeHead(status, {
@@ -1373,6 +1779,70 @@ function sendJson(res, status, data) {
1373
1779
  });
1374
1780
  res.end(body);
1375
1781
  }
1782
+ function sendHtml(res, status, html) {
1783
+ res.writeHead(status, {
1784
+ "Content-Type": "text/html; charset=utf-8",
1785
+ "Cache-Control": "no-store",
1786
+ ...SECURITY_HEADERS,
1787
+ ...CORS_HEADERS,
1788
+ });
1789
+ res.end(html);
1790
+ }
1791
+ function escapeHtml(value) {
1792
+ return value
1793
+ .replaceAll("&", "&amp;")
1794
+ .replaceAll("<", "&lt;")
1795
+ .replaceAll(">", "&gt;")
1796
+ .replaceAll('"', "&quot;")
1797
+ .replaceAll("'", "&#39;");
1798
+ }
1799
+ function resolveFilesystemOpenPath(rawPath) {
1800
+ let value = rawPath.trim();
1801
+ if (value.toLowerCase().startsWith("file://")) {
1802
+ value = value.replace(/^file:\/\//i, "");
1803
+ try {
1804
+ value = decodeURIComponent(value);
1805
+ }
1806
+ catch {
1807
+ // best effort
1808
+ }
1809
+ if (process.platform === "win32" && value.startsWith("/")) {
1810
+ value = value.slice(1);
1811
+ }
1812
+ }
1813
+ if (value.startsWith("~/")) {
1814
+ return resolve(homedir(), value.slice(2));
1815
+ }
1816
+ const looksWindowsAbsolute = /^[A-Za-z]:[\\/]/.test(value);
1817
+ if (value.startsWith("/") || looksWindowsAbsolute) {
1818
+ return resolve(value);
1819
+ }
1820
+ return resolve(process.cwd(), value);
1821
+ }
1822
+ function readFilePreview(pathname, totalBytes) {
1823
+ if (totalBytes <= 0) {
1824
+ return { previewBuffer: Buffer.alloc(0), truncated: false };
1825
+ }
1826
+ const previewBytes = Math.min(totalBytes, FILE_PREVIEW_MAX_BYTES);
1827
+ const previewBuffer = Buffer.alloc(previewBytes);
1828
+ const fd = openSync(pathname, "r");
1829
+ try {
1830
+ const bytesRead = readSync(fd, previewBuffer, 0, previewBytes, 0);
1831
+ if (bytesRead < previewBytes) {
1832
+ return {
1833
+ previewBuffer: previewBuffer.subarray(0, bytesRead),
1834
+ truncated: totalBytes > bytesRead,
1835
+ };
1836
+ }
1837
+ return {
1838
+ previewBuffer,
1839
+ truncated: totalBytes > previewBytes,
1840
+ };
1841
+ }
1842
+ finally {
1843
+ closeSync(fd);
1844
+ }
1845
+ }
1376
1846
  function sendFile(res, filePath, cacheControl) {
1377
1847
  try {
1378
1848
  const shouldCacheImmutable = cacheControl.includes("immutable");
@@ -2647,7 +3117,16 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2647
3117
  const message = input.message.trim();
2648
3118
  if (!message)
2649
3119
  return;
2650
- const enrichedMetadata = withProvenanceMetadata(input.metadata);
3120
+ const metadataWithProvenance = withProvenanceMetadata(input.metadata);
3121
+ const activityBucket = deriveStructuredActivityBucket({
3122
+ phase: input.phase,
3123
+ metadata: metadataWithProvenance,
3124
+ explicitBucket: input.activityBucket ?? null,
3125
+ });
3126
+ const enrichedMetadata = {
3127
+ ...(metadataWithProvenance ?? {}),
3128
+ activity_bucket: activityBucket,
3129
+ };
2651
3130
  try {
2652
3131
  await client.emitActivity({
2653
3132
  initiative_id: initiativeId,
@@ -2712,17 +3191,26 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2712
3191
  : "run_started",
2713
3192
  title: message,
2714
3193
  description: input.nextStep ?? null,
2715
- agentId: (typeof enrichedMetadata?.agent_id === "string"
2716
- ? enrichedMetadata.agent_id
3194
+ agentId: (typeof metadataWithProvenance?.agent_id === "string"
3195
+ ? metadataWithProvenance.agent_id
2717
3196
  : null) ?? null,
2718
- agentName: (typeof enrichedMetadata?.agent_name === "string"
2719
- ? enrichedMetadata.agent_name
3197
+ agentName: (typeof metadataWithProvenance?.agent_name === "string"
3198
+ ? metadataWithProvenance.agent_name
3199
+ : null) ?? null,
3200
+ requesterAgentId: null,
3201
+ requesterAgentName: null,
3202
+ executorAgentId: (typeof metadataWithProvenance?.agent_id === "string"
3203
+ ? metadataWithProvenance.agent_id
3204
+ : null) ?? null,
3205
+ executorAgentName: (typeof metadataWithProvenance?.agent_name === "string"
3206
+ ? metadataWithProvenance.agent_name
2720
3207
  : null) ?? null,
2721
3208
  runId,
2722
3209
  initiativeId,
2723
3210
  timestamp,
2724
3211
  phase: input.phase,
2725
3212
  summary: message,
3213
+ kind: activityBucket,
2726
3214
  metadata: {
2727
3215
  ...(enrichedMetadata ?? {}),
2728
3216
  source: "openclaw_local_fallback",
@@ -2807,6 +3295,10 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2807
3295
  description: input.summary ?? null,
2808
3296
  agentId: null,
2809
3297
  agentName: null,
3298
+ requesterAgentId: null,
3299
+ requesterAgentName: null,
3300
+ executorAgentId: null,
3301
+ executorAgentName: null,
2810
3302
  runId: correlationId ?? null,
2811
3303
  initiativeId,
2812
3304
  timestamp,
@@ -3157,10 +3649,22 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3157
3649
  }
3158
3650
  const autoContinueRuns = new Map();
3159
3651
  const localInitiativeStatusOverrides = new Map();
3160
- let autoContinueTickInFlight = false;
3161
- const AUTO_CONTINUE_TICK_MS = 2_500;
3652
+ let autoContinueTickInFlight = null;
3653
+ const AUTO_CONTINUE_TICK_MS = readBudgetEnvNumber("ORGX_AUTO_CONTINUE_TICK_MS", 2_500, {
3654
+ min: 250,
3655
+ max: 60_000,
3656
+ });
3162
3657
  const autoContinueSliceRuns = new Map();
3658
+ // Keep child handles alive so stdout/stderr capture remains reliable even when the process is detached.
3659
+ const autoContinueSliceChildren = new Map();
3163
3660
  const autoContinueSliceLastHeartbeatMs = new Map();
3661
+ const clearAutoContinueSliceTransientState = (sliceRunId) => {
3662
+ const id = (sliceRunId ?? "").trim();
3663
+ if (!id)
3664
+ return;
3665
+ autoContinueSliceChildren.delete(id);
3666
+ autoContinueSliceLastHeartbeatMs.delete(id);
3667
+ };
3164
3668
  const AUTO_CONTINUE_SLICE_MAX_TASKS = 6;
3165
3669
  const AUTO_CONTINUE_SLICE_TIMEOUT_MS = readBudgetEnvNumber("ORGX_AUTOPILOT_SLICE_TIMEOUT_MS", 55 * 60_000,
3166
3670
  // Keep test runs fast; real-world defaults are still ~1h unless overridden.
@@ -3287,7 +3791,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3287
3791
  async function updateInitiativeAutoContinueState(input) {
3288
3792
  const now = new Date().toISOString();
3289
3793
  const patch = {
3290
- auto_continue_enabled: true,
3794
+ auto_continue_enabled: input.run.status === "running" || input.run.status === "stopping",
3291
3795
  auto_continue_status: input.run.status,
3292
3796
  auto_continue_stop_reason: input.run.stopReason,
3293
3797
  auto_continue_started_at: input.run.startedAt,
@@ -3308,6 +3812,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3308
3812
  }
3309
3813
  async function stopAutoContinueRun(input) {
3310
3814
  const now = new Date().toISOString();
3815
+ const activeRunId = input.run.activeRunId;
3311
3816
  input.run.status = "stopped";
3312
3817
  input.run.stopReason = input.reason;
3313
3818
  input.run.stoppedAt = now;
@@ -3315,16 +3820,21 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3315
3820
  input.run.stopRequested = false;
3316
3821
  input.run.activeRunId = null;
3317
3822
  input.run.activeTaskId = null;
3823
+ input.run.activeTaskTokenEstimate = null;
3318
3824
  if (input.error)
3319
3825
  input.run.lastError = input.error;
3320
- // Autopilot should not auto-complete initiatives; keep status changes conservative.
3321
- try {
3322
- await client.updateEntity("initiative", input.run.initiativeId, {
3323
- status: "paused",
3324
- });
3325
- }
3326
- catch {
3327
- // best effort
3826
+ clearAutoContinueSliceTransientState(activeRunId);
3827
+ // Only pause the initiative on non-terminal stops (error, blocked, user-requested).
3828
+ // Completed / budget-exhausted runs should not override the initiative status.
3829
+ if (input.reason !== "completed" && input.reason !== "budget_exhausted") {
3830
+ try {
3831
+ await client.updateEntity("initiative", input.run.initiativeId, {
3832
+ status: "paused",
3833
+ });
3834
+ }
3835
+ catch {
3836
+ // best effort
3837
+ }
3328
3838
  }
3329
3839
  try {
3330
3840
  await updateInitiativeAutoContinueState({
@@ -3335,6 +3845,48 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3335
3845
  catch {
3336
3846
  // best effort
3337
3847
  }
3848
+ const scopeSuffix = Array.isArray(input.run.allowedWorkstreamIds) && input.run.allowedWorkstreamIds.length === 1
3849
+ ? ` (${input.run.allowedWorkstreamIds[0]})`
3850
+ : "";
3851
+ const message = input.reason === "completed"
3852
+ ? `Autopilot stopped: current dispatch scope completed${scopeSuffix}.`
3853
+ : input.reason === "budget_exhausted"
3854
+ ? `Autopilot stopped: token budget exhausted (${input.run.tokensUsed}/${input.run.tokenBudget}).`
3855
+ : input.reason === "stopped"
3856
+ ? `Autopilot stopped by user request${scopeSuffix}.`
3857
+ : input.reason === "blocked"
3858
+ ? `Autopilot stopped: blocked pending decision${scopeSuffix}.`
3859
+ : `Autopilot stopped due to error${scopeSuffix}.`;
3860
+ const phase = input.reason === "completed"
3861
+ ? "completed"
3862
+ : input.reason === "blocked" || input.reason === "error"
3863
+ ? "blocked"
3864
+ : "review";
3865
+ const level = input.reason === "completed"
3866
+ ? "info"
3867
+ : input.reason === "budget_exhausted" || input.reason === "stopped"
3868
+ ? "warn"
3869
+ : "error";
3870
+ await emitActivitySafe({
3871
+ initiativeId: input.run.initiativeId,
3872
+ runId: activeRunId ?? input.run.lastRunId ?? undefined,
3873
+ correlationId: activeRunId ?? input.run.lastRunId ?? undefined,
3874
+ phase,
3875
+ level,
3876
+ message,
3877
+ metadata: {
3878
+ event: "auto_continue_stopped",
3879
+ stop_reason: input.reason,
3880
+ requested_by_agent_id: input.run.agentId,
3881
+ requested_by_agent_name: input.run.agentName,
3882
+ active_run_id: activeRunId,
3883
+ last_run_id: input.run.lastRunId,
3884
+ token_budget: input.run.tokenBudget,
3885
+ tokens_used: input.run.tokensUsed,
3886
+ allowed_workstream_ids: input.run.allowedWorkstreamIds,
3887
+ last_error: input.run.lastError,
3888
+ },
3889
+ });
3338
3890
  }
3339
3891
  function ensurePrivateDirForFile(pathname) {
3340
3892
  const dir = dirname(pathname);
@@ -3460,11 +4012,54 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3460
4012
  const direct = parseJsonSafe(trimmed);
3461
4013
  if (direct && typeof direct === "object")
3462
4014
  return direct;
3463
- // Tolerant parse: grab the last JSON object-looking blob.
3464
- const first = trimmed.lastIndexOf("{");
3465
- const last = trimmed.lastIndexOf("}");
3466
- if (first >= 0 && last > first) {
3467
- const candidate = trimmed.slice(first, last + 1);
4015
+ // Tolerant parse: extract the last complete top-level JSON object from mixed logs.
4016
+ // (Workers sometimes append multiple JSON payloads, or wrap output with extra text.)
4017
+ const extractLastTopLevelObject = (text) => {
4018
+ let inString = false;
4019
+ let escaped = false;
4020
+ let depth = 0;
4021
+ let start = -1;
4022
+ let lastObject = null;
4023
+ for (let i = 0; i < text.length; i += 1) {
4024
+ const ch = text[i];
4025
+ if (inString) {
4026
+ if (escaped) {
4027
+ escaped = false;
4028
+ continue;
4029
+ }
4030
+ if (ch === "\\") {
4031
+ escaped = true;
4032
+ continue;
4033
+ }
4034
+ if (ch === "\"") {
4035
+ inString = false;
4036
+ }
4037
+ continue;
4038
+ }
4039
+ if (ch === "\"") {
4040
+ inString = true;
4041
+ continue;
4042
+ }
4043
+ if (ch === "{") {
4044
+ if (depth === 0)
4045
+ start = i;
4046
+ depth += 1;
4047
+ continue;
4048
+ }
4049
+ if (ch === "}") {
4050
+ if (depth <= 0)
4051
+ continue;
4052
+ depth -= 1;
4053
+ if (depth === 0 && start >= 0) {
4054
+ lastObject = text.slice(start, i + 1);
4055
+ start = -1;
4056
+ }
4057
+ }
4058
+ }
4059
+ return lastObject;
4060
+ };
4061
+ const candidate = extractLastTopLevelObject(trimmed);
4062
+ if (candidate) {
3468
4063
  const parsed = parseJsonSafe(candidate);
3469
4064
  if (parsed && typeof parsed === "object")
3470
4065
  return parsed;
@@ -3717,8 +4312,16 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3717
4312
  ...input.env,
3718
4313
  },
3719
4314
  stdio: ["ignore", "pipe", "pipe"],
3720
- detached: true,
4315
+ // Keep the mock worker as a normal child so stdout/stderr capture is deterministic.
4316
+ detached: false,
3721
4317
  });
4318
+ autoContinueSliceChildren.set(input.runId, child);
4319
+ try {
4320
+ logStream.write(`spawned pid=${String(child.pid ?? "")} stdout=${String(Boolean(child.stdout))} stderr=${String(Boolean(child.stderr))}\n`);
4321
+ }
4322
+ catch {
4323
+ // ignore
4324
+ }
3722
4325
  child.stdout?.on("data", (chunk) => {
3723
4326
  try {
3724
4327
  logStream.write(chunk);
@@ -3742,6 +4345,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3742
4345
  }
3743
4346
  });
3744
4347
  child.on("close", (code, signal) => {
4348
+ autoContinueSliceChildren.delete(input.runId);
3745
4349
  const stamp = new Date().toISOString();
3746
4350
  try {
3747
4351
  logStream.write(`\n==== ${stamp} :: exit code=${String(code)} signal=${String(signal)} ====\n`);
@@ -3763,6 +4367,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3763
4367
  }
3764
4368
  });
3765
4369
  child.on("error", (error) => {
4370
+ autoContinueSliceChildren.delete(input.runId);
3766
4371
  const msg = safeErrorMessage(error);
3767
4372
  try {
3768
4373
  logStream.write(`\nworker error: ${msg}\n`);
@@ -3783,7 +4388,6 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3783
4388
  // ignore
3784
4389
  }
3785
4390
  });
3786
- child.unref();
3787
4391
  return { pid: child.pid ?? null };
3788
4392
  }
3789
4393
  if (workerKind === "claude-code" || workerKind === "claude_code") {
@@ -3805,6 +4409,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3805
4409
  stdio: ["ignore", "pipe", "pipe"],
3806
4410
  detached: true,
3807
4411
  });
4412
+ autoContinueSliceChildren.set(input.runId, child);
3808
4413
  child.stdout?.on("data", (chunk) => {
3809
4414
  try {
3810
4415
  logStream.write(chunk);
@@ -3828,6 +4433,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3828
4433
  }
3829
4434
  });
3830
4435
  child.on("close", (code, signal) => {
4436
+ autoContinueSliceChildren.delete(input.runId);
3831
4437
  const stamp = new Date().toISOString();
3832
4438
  try {
3833
4439
  logStream.write(`\n==== ${stamp} :: exit code=${String(code)} signal=${String(signal)} ====\n`);
@@ -3849,6 +4455,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3849
4455
  }
3850
4456
  });
3851
4457
  child.on("error", (error) => {
4458
+ autoContinueSliceChildren.delete(input.runId);
3852
4459
  const msg = safeErrorMessage(error);
3853
4460
  try {
3854
4461
  logStream.write(`\nworker error: ${msg}\n`);
@@ -3908,6 +4515,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3908
4515
  stdio: ["ignore", "pipe", "pipe"],
3909
4516
  detached: true,
3910
4517
  });
4518
+ autoContinueSliceChildren.set(input.runId, child);
3911
4519
  child.stdout?.on("data", (chunk) => {
3912
4520
  try {
3913
4521
  logStream.write(chunk);
@@ -3925,6 +4533,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3925
4533
  }
3926
4534
  });
3927
4535
  child.on("close", (code, signal) => {
4536
+ autoContinueSliceChildren.delete(input.runId);
3928
4537
  const stamp = new Date().toISOString();
3929
4538
  try {
3930
4539
  logStream.write(`\n==== ${stamp} :: exit code=${String(code)} signal=${String(signal)} ====\n`);
@@ -3940,6 +4549,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3940
4549
  }
3941
4550
  });
3942
4551
  child.on("error", (error) => {
4552
+ autoContinueSliceChildren.delete(input.runId);
3943
4553
  const msg = safeErrorMessage(error);
3944
4554
  try {
3945
4555
  logStream.write(`\nworker error: ${msg}\n`);
@@ -3982,6 +4592,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
3982
4592
  });
3983
4593
  // Make runtime updates feel instantaneous (don't wait for the 15s staleness timer).
3984
4594
  broadcastRuntimeSse("runtime.updated", instance);
4595
+ clearSnapshotResponseCache();
3985
4596
  return instance;
3986
4597
  }
3987
4598
  function buildWorkstreamSlicePrompt(input) {
@@ -4026,6 +4637,10 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4026
4637
  `- Your JSON MUST conform to this schema file: ${input.schemaPath}`,
4027
4638
  `- Artifacts must be verifiable: include URLs or local paths, plus verification steps.`,
4028
4639
  `- If you need a human decision, include it in decisions_needed.`,
4640
+ `- For every decisions_needed entry, ALWAYS set blocking explicitly (true or false).`,
4641
+ `- Status/decision consistency is strict:`,
4642
+ ` - If any decision is blocking=true, status MUST be needs_decision or blocked (never completed).`,
4643
+ ` - Only use status=completed when all listed decisions are non-blocking follow-ups.`,
4029
4644
  `- If you are confident OrgX statuses should change, include task_updates and/or milestone_updates (with a short reason).`,
4030
4645
  ` - task_updates.status must be one of: todo, in_progress, done, blocked`,
4031
4646
  ` - milestone_updates.status must be one of: planned, in_progress, completed, at_risk, cancelled`,
@@ -4037,6 +4652,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4037
4652
  if (!name)
4038
4653
  return { ok: false, id: null };
4039
4654
  const artifactType = (input.artifact.artifact_type ?? "other").trim() || "other";
4655
+ const artifactId = randomUUID();
4040
4656
  const verificationSteps = Array.isArray(input.artifact.verification_steps)
4041
4657
  ? input.artifact.verification_steps
4042
4658
  .filter((step) => typeof step === "string")
@@ -4051,23 +4667,38 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4051
4667
  descriptionParts.push(`Verification:\n${verificationSteps.map((step) => `- ${step}`).join("\n")}`);
4052
4668
  }
4053
4669
  const description = descriptionParts.length > 0 ? descriptionParts.join("\n\n") : undefined;
4670
+ const hasUuidAgent = typeof input.agentId === "string" &&
4671
+ /^[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(input.agentId);
4672
+ const createdByType = hasUuidAgent ? "agent" : "human";
4673
+ const createdById = hasUuidAgent ? input.agentId : null;
4054
4674
  try {
4055
- const entity = await client.createEntity("artifact", {
4056
- initiative_id: input.initiativeId,
4057
- workstream_id: input.workstreamId,
4058
- title: name,
4675
+ const entityType = input.artifact.milestone_id ? "milestone" : "initiative";
4676
+ const entityId = input.artifact.milestone_id ? input.artifact.milestone_id : input.initiativeId;
4677
+ const result = await registerArtifact(client, client.getBaseUrl(), {
4678
+ artifact_id: artifactId,
4679
+ entity_type: entityType,
4680
+ entity_id: entityId,
4681
+ name,
4059
4682
  artifact_type: artifactType,
4683
+ created_by_type: createdByType,
4684
+ created_by_id: createdById,
4060
4685
  description,
4061
- artifact_url: input.artifact.url ?? undefined,
4062
- status: "active",
4686
+ external_url: input.artifact.url ?? null,
4687
+ preview_markdown: null,
4688
+ status: "draft",
4063
4689
  metadata: {
4064
4690
  source: "autopilot_slice",
4691
+ artifact_id: artifactId,
4065
4692
  run_id: input.runId,
4693
+ initiative_id: input.initiativeId,
4694
+ workstream_id: input.workstreamId,
4066
4695
  milestone_id: input.artifact.milestone_id ?? null,
4067
4696
  task_ids: input.artifact.task_ids ?? null,
4068
4697
  },
4698
+ // Make persistence validation opt-in to avoid adding latency to every slice by default.
4699
+ validate_persistence: process.env.ORGX_VALIDATE_ARTIFACT_PERSISTENCE === "1",
4069
4700
  });
4070
- return { ok: true, id: pickString(entity, ["id"]) ?? null };
4701
+ return { ok: result.ok, id: result.artifact_id };
4071
4702
  }
4072
4703
  catch (err) {
4073
4704
  try {
@@ -4076,10 +4707,13 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4076
4707
  type: "artifact",
4077
4708
  timestamp: now,
4078
4709
  payload: {
4079
- initiative_id: input.initiativeId,
4080
- workstream_id: input.workstreamId,
4710
+ artifact_id: artifactId,
4711
+ entity_type: input.artifact.milestone_id ? "milestone" : "initiative",
4712
+ entity_id: input.artifact.milestone_id ?? input.initiativeId,
4081
4713
  name,
4082
4714
  artifact_type: artifactType,
4715
+ created_by_type: createdByType,
4716
+ created_by_id: createdById,
4083
4717
  description,
4084
4718
  url: input.artifact.url ?? undefined,
4085
4719
  run_id: input.runId,
@@ -4091,6 +4725,10 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4091
4725
  description: description ?? null,
4092
4726
  agentId: input.agentId,
4093
4727
  agentName: input.agentName ?? null,
4728
+ requesterAgentId: input.agentId,
4729
+ requesterAgentName: input.agentName ?? null,
4730
+ executorAgentId: input.agentId,
4731
+ executorAgentName: input.agentName ?? null,
4094
4732
  runId: input.runId,
4095
4733
  initiativeId: input.initiativeId,
4096
4734
  timestamp: now,
@@ -4100,6 +4738,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4100
4738
  source: "openclaw_local_fallback",
4101
4739
  event: "autopilot_slice_artifact_buffered",
4102
4740
  artifact_type: artifactType,
4741
+ artifact_id: artifactId,
4103
4742
  url: input.artifact.url ?? null,
4104
4743
  error: safeErrorMessage(err),
4105
4744
  },
@@ -4228,6 +4867,10 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4228
4867
  description: null,
4229
4868
  agentId: null,
4230
4869
  agentName: null,
4870
+ requesterAgentId: null,
4871
+ requesterAgentName: null,
4872
+ executorAgentId: null,
4873
+ executorAgentName: null,
4231
4874
  runId: input.runId,
4232
4875
  initiativeId: input.initiativeId,
4233
4876
  timestamp,
@@ -4330,6 +4973,9 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4330
4973
  metadata: {
4331
4974
  event: "next_up_manual_dispatch_started",
4332
4975
  agent_id: input.agentId,
4976
+ agent_name: input.agentName ?? input.agentId,
4977
+ requested_by_agent_id: input.agentId,
4978
+ requested_by_agent_name: input.agentName ?? input.agentId,
4333
4979
  session_id: sessionId,
4334
4980
  workstream_id: input.workstreamId,
4335
4981
  workstream_title: resolvedWorkstreamTitle,
@@ -4384,6 +5030,32 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4384
5030
  spawnGuardResult: guard.spawnGuardResult,
4385
5031
  };
4386
5032
  }
5033
+ async function resolveAgentDisplayName(agentId, fallbackName) {
5034
+ const normalizedAgentId = agentId.trim();
5035
+ if (!normalizedAgentId)
5036
+ return null;
5037
+ const normalizedFallback = typeof fallbackName === "string" && fallbackName.trim().length > 0
5038
+ ? fallbackName.trim()
5039
+ : null;
5040
+ try {
5041
+ const agents = await listAgents();
5042
+ for (const entry of agents) {
5043
+ const record = entry;
5044
+ const candidateId = pickString(record, ["id", "agent_id", "agentId"]);
5045
+ if (!candidateId || candidateId.trim() !== normalizedAgentId)
5046
+ continue;
5047
+ const candidateName = pickString(record, ["name", "agent_name", "agentName"]);
5048
+ if (candidateName && candidateName.trim().length > 0) {
5049
+ return candidateName.trim();
5050
+ }
5051
+ break;
5052
+ }
5053
+ }
5054
+ catch {
5055
+ // best effort
5056
+ }
5057
+ return normalizedFallback ?? normalizedAgentId;
5058
+ }
4387
5059
  async function tickAutoContinueRun(run) {
4388
5060
  if (run.status !== "running" && run.status !== "stopping")
4389
5061
  return;
@@ -4408,13 +5080,15 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4408
5080
  typeof outputParsed.summary === "string");
4409
5081
  if (outputComplete) {
4410
5082
  // Some platforms can report a just-finished detached process as still "alive" (zombie).
4411
- // Best-effort stop so we can proceed to parse the output contract below.
5083
+ // Best-effort stop, then clear pid so we can proceed to parse the output contract below.
4412
5084
  try {
4413
5085
  await stopProcess(pid);
4414
5086
  }
4415
5087
  catch {
4416
5088
  // best effort
4417
5089
  }
5090
+ slice.pid = null;
5091
+ autoContinueSliceRuns.set(slice.runId, slice);
4418
5092
  }
4419
5093
  else {
4420
5094
  const lastHeartbeat = autoContinueSliceLastHeartbeatMs.get(slice.runId) ?? 0;
@@ -4433,6 +5107,8 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4433
5107
  message: `Autopilot slice running: ${slice.workstreamTitle ?? slice.workstreamId}`,
4434
5108
  metadata: {
4435
5109
  event: "autopilot_slice_heartbeat",
5110
+ requested_by_agent_id: run.agentId,
5111
+ requested_by_agent_name: run.agentName,
4436
5112
  domain: slice.domain,
4437
5113
  required_skills: slice.requiredSkills,
4438
5114
  workstream_id: slice.workstreamId,
@@ -4470,6 +5146,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4470
5146
  autoContinueSliceRuns.set(slice.runId, slice);
4471
5147
  run.lastError = slice.lastError;
4472
5148
  run.updatedAt = now;
5149
+ clearAutoContinueSliceTransientState(slice.runId);
4473
5150
  await emitActivitySafe({
4474
5151
  initiativeId: run.initiativeId,
4475
5152
  runId: slice.runId,
@@ -4479,6 +5156,8 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4479
5156
  message: `Autopilot slice MCP failed: ${slice.workstreamTitle ?? slice.workstreamId}.`,
4480
5157
  metadata: {
4481
5158
  event: "autopilot_slice_mcp_handshake_failed",
5159
+ requested_by_agent_id: run.agentId,
5160
+ requested_by_agent_name: run.agentName,
4482
5161
  mcp_server: mcpHandshake.server,
4483
5162
  mcp_line: mcpHandshake.line,
4484
5163
  workstream_id: slice.workstreamId,
@@ -4530,6 +5209,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4530
5209
  autoContinueSliceRuns.set(slice.runId, slice);
4531
5210
  run.lastError = slice.lastError;
4532
5211
  run.updatedAt = now;
5212
+ clearAutoContinueSliceTransientState(slice.runId);
4533
5213
  const event = killDecision.kind === "timeout" ? "autopilot_slice_timeout" : "autopilot_slice_log_stall";
4534
5214
  const humanLabel = killDecision.kind === "timeout" ? "timed out" : "stalled";
4535
5215
  await emitActivitySafe({
@@ -4541,6 +5221,8 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4541
5221
  message: `Autopilot slice ${humanLabel}: ${slice.workstreamTitle ?? slice.workstreamId}.`,
4542
5222
  metadata: {
4543
5223
  event,
5224
+ requested_by_agent_id: run.agentId,
5225
+ requested_by_agent_name: run.agentName,
4544
5226
  workstream_id: slice.workstreamId,
4545
5227
  task_ids: slice.taskIds,
4546
5228
  milestone_ids: slice.milestoneIds,
@@ -4579,17 +5261,28 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4579
5261
  // best effort
4580
5262
  }
4581
5263
  }
4582
- return;
5264
+ if (!outputComplete)
5265
+ return;
4583
5266
  }
4584
5267
  }
4585
5268
  // Slice finished.
4586
5269
  const raw = readSliceOutputFile(slice.outputPath);
4587
5270
  const parsed = raw ? parseSliceResult(raw) : null;
4588
5271
  const parsedStatus = parsed?.status ?? "error";
5272
+ const defaultDecisionBlocking = parsedStatus === "completed" ? false : true;
5273
+ const decisions = Array.isArray(parsed?.decisions_needed)
5274
+ ? (parsed?.decisions_needed ?? [])
5275
+ .filter((item) => Boolean(item && typeof item.question === "string" && item.question.trim()))
5276
+ : [];
5277
+ const blockingDecisionCount = decisions.filter((item) => typeof item.blocking === "boolean" ? item.blocking : defaultDecisionBlocking).length;
5278
+ const nonBlockingDecisionCount = Math.max(0, decisions.length - blockingDecisionCount);
5279
+ const effectiveParsedStatus = parsedStatus === "completed" && blockingDecisionCount > 0
5280
+ ? "needs_decision"
5281
+ : parsedStatus;
4589
5282
  slice.status =
4590
- parsedStatus === "completed"
5283
+ effectiveParsedStatus === "completed"
4591
5284
  ? "completed"
4592
- : parsedStatus === "blocked" || parsedStatus === "needs_decision"
5285
+ : effectiveParsedStatus === "blocked" || effectiveParsedStatus === "needs_decision"
4593
5286
  ? "blocked"
4594
5287
  : "error";
4595
5288
  slice.finishedAt = now;
@@ -4599,14 +5292,11 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4599
5292
  ? slice.lastError ?? "Autopilot slice failed or returned invalid output."
4600
5293
  : null;
4601
5294
  autoContinueSliceRuns.set(slice.runId, slice);
5295
+ clearAutoContinueSliceTransientState(slice.runId);
4602
5296
  // Token accounting: codex CLI doesn't provide tokens here; use the modeled estimate.
4603
5297
  const modeledTokens = slice.tokenEstimate ?? run.activeTaskTokenEstimate ?? 0;
4604
5298
  run.tokensUsed += Math.max(0, modeledTokens);
4605
5299
  run.activeTaskTokenEstimate = null;
4606
- const decisions = Array.isArray(parsed?.decisions_needed)
4607
- ? (parsed?.decisions_needed ?? [])
4608
- .filter((item) => Boolean(item && typeof item.question === "string" && item.question.trim()))
4609
- : [];
4610
5300
  const artifacts = Array.isArray(parsed?.artifacts)
4611
5301
  ? (parsed?.artifacts ?? [])
4612
5302
  .filter((item) => Boolean(item && typeof item.name === "string" && item.name.trim()))
@@ -4627,7 +5317,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4627
5317
  options: Array.isArray(decision.options)
4628
5318
  ? decision.options.filter((opt) => typeof opt === "string" && opt.trim())
4629
5319
  : [],
4630
- blocking: typeof decision.blocking === "boolean" ? decision.blocking : true,
5320
+ blocking: typeof decision.blocking === "boolean" ? decision.blocking : defaultDecisionBlocking,
4631
5321
  });
4632
5322
  }
4633
5323
  for (const artifact of artifacts) {
@@ -4661,9 +5351,13 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4661
5351
  message: parsed?.summary ?? slice.lastError ?? "Autopilot slice finished.",
4662
5352
  metadata: {
4663
5353
  event: "autopilot_slice_finished",
4664
- status: parsedStatus,
5354
+ requested_by_agent_id: run.agentId,
5355
+ requested_by_agent_name: run.agentName,
5356
+ status: effectiveParsedStatus,
4665
5357
  artifacts: artifacts.length,
4666
5358
  decisions: decisions.length,
5359
+ blocking_decisions: blockingDecisionCount,
5360
+ non_blocking_decisions: nonBlockingDecisionCount,
4667
5361
  status_updates: statusUpdateResult.applied,
4668
5362
  status_updates_buffered: statusUpdateResult.buffered,
4669
5363
  },
@@ -4679,10 +5373,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4679
5373
  phase: slice.status === "completed" ? "completed" : "blocked",
4680
5374
  level: slice.status === "completed" ? "info" : "warn",
4681
5375
  message: slice.status === "completed"
4682
- ? `Autopilot slice completed: ${slice.workstreamTitle ?? slice.workstreamId}.`
5376
+ ? `Autopilot slice completed for ${slice.workstreamTitle ?? slice.workstreamId} (${slice.taskIds.length} task${slice.taskIds.length === 1 ? "" : "s"}).`
4683
5377
  : `Autopilot slice blocked: ${slice.workstreamTitle ?? slice.workstreamId}.`,
4684
5378
  metadata: {
4685
5379
  event: "autopilot_slice_result",
5380
+ requested_by_agent_id: run.agentId,
5381
+ requested_by_agent_name: run.agentName,
4686
5382
  agent_id: slice.agentId,
4687
5383
  agent_name: slice.agentName,
4688
5384
  domain: slice.domain,
@@ -4690,10 +5386,13 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4690
5386
  workstream_id: slice.workstreamId,
4691
5387
  task_ids: slice.taskIds,
4692
5388
  milestone_ids: slice.milestoneIds,
4693
- parsed_status: parsedStatus,
5389
+ parsed_status: effectiveParsedStatus,
4694
5390
  has_output: Boolean(parsed),
4695
5391
  artifacts: artifacts.length,
4696
5392
  decisions: decisions.length,
5393
+ blocking_decisions: blockingDecisionCount,
5394
+ non_blocking_decisions: nonBlockingDecisionCount,
5395
+ decision_required: blockingDecisionCount > 0,
4697
5396
  status_updates_applied: statusUpdateResult.applied,
4698
5397
  status_updates_buffered: statusUpdateResult.buffered,
4699
5398
  output_path: slice.outputPath,
@@ -4724,7 +5423,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4724
5423
  reason: slice.status === "error" ? "error" : "blocked",
4725
5424
  error: parsed?.summary ??
4726
5425
  slice.lastError ??
4727
- `Slice returned status: ${parsedStatus}`,
5426
+ `Slice returned status: ${effectiveParsedStatus}`,
4728
5427
  });
4729
5428
  return;
4730
5429
  }
@@ -4839,7 +5538,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4839
5538
  continue;
4840
5539
  if (!run.includeVerification &&
4841
5540
  typeof node.title === "string" &&
4842
- /^verification\\s+scenario/i.test(node.title)) {
5541
+ /^verification[ \t]+scenario/i.test(node.title)) {
4843
5542
  continue;
4844
5543
  }
4845
5544
  if (run.allowedWorkstreamIds && node.workstreamId) {
@@ -4875,7 +5574,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
4875
5574
  taskIsReady(node) &&
4876
5575
  !taskHasBlockedParent(node) &&
4877
5576
  (run.includeVerification ||
4878
- !/^verification\\s+scenario/i.test(String(node.title ?? "")))))
5577
+ !/^verification[ \t]+scenario/i.test(String(node.title ?? "")))))
4879
5578
  .slice(0, AUTO_CONTINUE_SLICE_MAX_TASKS);
4880
5579
  const primaryTask = sliceTaskNodes[0] ?? null;
4881
5580
  if (!primaryTask) {
@@ -5055,6 +5754,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
5055
5754
  ORGX_TASK_ID: primaryTask.id,
5056
5755
  ORGX_AGENT_ID: sliceAgent.id,
5057
5756
  ORGX_AGENT_NAME: sliceAgent.name,
5757
+ ORGX_OUTPUT_PATH: outputPath,
5058
5758
  ORGX_RUNTIME_HOOK_URL: runtimeHookUrl ?? undefined,
5059
5759
  ORGX_HOOK_TOKEN: runtimeHookToken ?? undefined,
5060
5760
  },
@@ -5097,6 +5797,8 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
5097
5797
  message: `Autopilot slice started: ${workstreamTitle ?? selectedWorkstreamId}`,
5098
5798
  metadata: {
5099
5799
  event: "autopilot_slice_started",
5800
+ requested_by_agent_id: run.agentId,
5801
+ requested_by_agent_name: run.agentName,
5100
5802
  domain: executionPolicy.domain,
5101
5803
  required_skills: executionPolicy.requiredSkills,
5102
5804
  task_ids: slice.taskIds,
@@ -5120,6 +5822,8 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
5120
5822
  message: `Autopilot dispatched slice for ${workstreamTitle ?? selectedWorkstreamId}.`,
5121
5823
  metadata: {
5122
5824
  event: "autopilot_slice_dispatched",
5825
+ requested_by_agent_id: run.agentId,
5826
+ requested_by_agent_name: run.agentName,
5123
5827
  agent_id: slice.agentId,
5124
5828
  agent_name: sliceAgent.name,
5125
5829
  domain: executionPolicy.domain,
@@ -5163,10 +5867,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
5163
5867
  }
5164
5868
  }
5165
5869
  async function tickAllAutoContinue() {
5166
- if (autoContinueTickInFlight)
5870
+ if (autoContinueTickInFlight) {
5871
+ // Wait for the in-flight tick to finish instead of silently dropping.
5872
+ await autoContinueTickInFlight.catch(() => { });
5167
5873
  return;
5168
- autoContinueTickInFlight = true;
5169
- try {
5874
+ }
5875
+ const work = (async () => {
5170
5876
  for (const run of autoContinueRuns.values()) {
5171
5877
  try {
5172
5878
  await tickAutoContinueRun(run);
@@ -5178,9 +5884,13 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
5178
5884
  await stopAutoContinueRun({ run, reason: "error", error: run.lastError });
5179
5885
  }
5180
5886
  }
5887
+ })();
5888
+ autoContinueTickInFlight = work;
5889
+ try {
5890
+ await work;
5181
5891
  }
5182
5892
  finally {
5183
- autoContinueTickInFlight = false;
5893
+ autoContinueTickInFlight = null;
5184
5894
  }
5185
5895
  }
5186
5896
  function isInitiativeActiveStatus(status) {
@@ -5207,10 +5917,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
5207
5917
  async function startAutoContinueRun(input) {
5208
5918
  const now = new Date().toISOString();
5209
5919
  const existing = autoContinueRuns.get(input.initiativeId) ?? null;
5920
+ const existingIsLive = existing?.status === "running" || existing?.status === "stopping";
5210
5921
  const run = existing ??
5211
5922
  {
5212
5923
  initiativeId: input.initiativeId,
5213
5924
  agentId: input.agentId,
5925
+ agentName: input.agentName ?? null,
5214
5926
  includeVerification: false,
5215
5927
  allowedWorkstreamIds: null,
5216
5928
  stopAfterSlice: false,
@@ -5230,6 +5942,10 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
5230
5942
  activeTaskTokenEstimate: null,
5231
5943
  };
5232
5944
  run.agentId = input.agentId;
5945
+ run.agentName =
5946
+ typeof input.agentName === "string" && input.agentName.trim().length > 0
5947
+ ? input.agentName.trim()
5948
+ : null;
5233
5949
  run.includeVerification = input.includeVerification;
5234
5950
  run.allowedWorkstreamIds = input.allowedWorkstreamIds;
5235
5951
  run.stopAfterSlice = Boolean(input.stopAfterSlice);
@@ -5237,10 +5953,19 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
5237
5953
  run.status = "running";
5238
5954
  run.stopReason = null;
5239
5955
  run.stopRequested = false;
5240
- run.startedAt = now;
5241
5956
  run.stoppedAt = null;
5242
5957
  run.updatedAt = now;
5243
5958
  run.lastError = null;
5959
+ const forceFreshRun = Boolean(input.stopAfterSlice);
5960
+ if (!existingIsLive || forceFreshRun) {
5961
+ run.tokensUsed = 0;
5962
+ run.startedAt = now;
5963
+ run.lastTaskId = null;
5964
+ run.lastRunId = null;
5965
+ run.activeTaskId = null;
5966
+ run.activeRunId = null;
5967
+ run.activeTaskTokenEstimate = null;
5968
+ }
5244
5969
  autoContinueRuns.set(input.initiativeId, run);
5245
5970
  try {
5246
5971
  await client.updateEntity("initiative", input.initiativeId, { status: "active" });
@@ -5784,6 +6509,8 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
5784
6509
  const isEntitiesRoute = route === "entities";
5785
6510
  const entityCommentsMatch = route.match(/^entities\/([^/]+)\/([^/]+)\/comments$/);
5786
6511
  const entityActionMatch = route.match(/^entities\/([^/]+)\/([^/]+)\/([^/]+)$/);
6512
+ const isArtifactsByEntityRoute = route === "work-artifacts/by-entity";
6513
+ const artifactDetailMatch = route.match(/^artifacts\/([^/]+)$/);
5787
6514
  const isOnboardingStartRoute = route === "onboarding/start";
5788
6515
  const isOnboardingStatusRoute = route === "onboarding/status";
5789
6516
  const isOnboardingManualKeyRoute = route === "onboarding/manual-key";
@@ -6306,6 +7033,23 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
6306
7033
  }
6307
7034
  const result = await stopProcess(record.pid);
6308
7035
  const updated = markAgentRunStopped(runId);
7036
+ try {
7037
+ writeRuntimeEvent({
7038
+ sourceClient: "codex",
7039
+ event: "session_stop",
7040
+ runId,
7041
+ initiativeId: record.initiativeId ?? "",
7042
+ workstreamId: record.workstreamId ?? null,
7043
+ taskId: record.taskId ?? null,
7044
+ agentId: record.agentId ?? null,
7045
+ agentName: null,
7046
+ phase: "stopped",
7047
+ message: `Agent ${record.agentId ?? "unknown"} stopped.`,
7048
+ });
7049
+ }
7050
+ catch {
7051
+ // best effort
7052
+ }
6309
7053
  void posthogCapture({
6310
7054
  event: "openclaw_agent_stop",
6311
7055
  distinctId: telemetryDistinctId,
@@ -6363,6 +7107,17 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
6363
7107
  sendJson(res, 404, { ok: false, error: "Run not found" });
6364
7108
  return true;
6365
7109
  }
7110
+ // Stop the previous process before spawning a replacement to avoid
7111
+ // orphaned workers consuming resources or writing conflicting output.
7112
+ if (record.pid) {
7113
+ try {
7114
+ await stopProcess(record.pid);
7115
+ }
7116
+ catch {
7117
+ // best effort — process may have already exited
7118
+ }
7119
+ markAgentRunStopped(previousRunId);
7120
+ }
6366
7121
  const messageOverride = (pickString(payload, ["message", "prompt", "text"]) ??
6367
7122
  searchParams.get("message") ??
6368
7123
  searchParams.get("prompt") ??
@@ -6578,6 +7333,9 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
6578
7333
  });
6579
7334
  return true;
6580
7335
  }
7336
+ const requestedAgentName = await resolveAgentDisplayName(agentId, matchedQueueItem?.runnerAgentId === agentId
7337
+ ? matchedQueueItem.runnerAgentName
7338
+ : null);
6581
7339
  // Autopilot v2 runs slices via local codex dispatch, so BYOK plan gating does not apply here.
6582
7340
  const tokenBudget = pickNumber(payload, [
6583
7341
  "tokenBudget",
@@ -6612,9 +7370,29 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
6612
7370
  const fastAck = typeof fastAckRaw === "boolean"
6613
7371
  ? fastAckRaw
6614
7372
  : parseBooleanQuery(typeof fastAckRaw === "string" ? fastAckRaw : null);
7373
+ const existingRun = autoContinueRuns.get(initiativeId) ?? null;
7374
+ if (existingRun &&
7375
+ (existingRun.status === "running" || existingRun.status === "stopping") &&
7376
+ existingRun.activeRunId) {
7377
+ const activeSlice = autoContinueSliceRuns.get(existingRun.activeRunId) ?? null;
7378
+ const activeWorkstreamId = activeSlice?.workstreamId ?? null;
7379
+ const activeWorkstreamTitle = activeSlice?.workstreamTitle ?? null;
7380
+ sendJson(res, 409, {
7381
+ ok: false,
7382
+ code: "auto_continue_already_running",
7383
+ error: activeWorkstreamId || activeWorkstreamTitle
7384
+ ? `Auto-continue is already running for ${activeWorkstreamTitle ?? activeWorkstreamId}. Stop it before launching another Play run.`
7385
+ : "Auto-continue is already running for this initiative. Stop it before launching another Play run.",
7386
+ run: existingRun,
7387
+ activeWorkstreamId,
7388
+ activeWorkstreamTitle,
7389
+ });
7390
+ return true;
7391
+ }
6615
7392
  const run = await startAutoContinueRun({
6616
7393
  initiativeId,
6617
7394
  agentId,
7395
+ agentName: requestedAgentName,
6618
7396
  tokenBudget,
6619
7397
  includeVerification,
6620
7398
  allowedWorkstreamIds: [workstreamId],
@@ -6631,6 +7409,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
6631
7409
  workstreamId,
6632
7410
  workstreamTitle: matchedQueueItem.workstreamTitle,
6633
7411
  agentId,
7412
+ agentName: requestedAgentName,
6634
7413
  taskId: matchedQueueItem.nextTaskId ?? null,
6635
7414
  taskTitle: matchedQueueItem.nextTaskTitle ?? null,
6636
7415
  });
@@ -6880,6 +7659,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
6880
7659
  const run = await startAutoContinueRun({
6881
7660
  initiativeId,
6882
7661
  agentId,
7662
+ agentName: await resolveAgentDisplayName(agentId, null),
6883
7663
  tokenBudget,
6884
7664
  includeVerification,
6885
7665
  allowedWorkstreamIds,
@@ -7120,6 +7900,40 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
7120
7900
  }
7121
7901
  return true;
7122
7902
  }
7903
+ // Work artifacts by entity: GET /orgx/api/work-artifacts/by-entity?entity_type=...&entity_id=...&limit=...&status=...
7904
+ if (isArtifactsByEntityRoute && method === "GET") {
7905
+ try {
7906
+ const qs = rawUrl.includes("?") ? rawUrl.split("?")[1] : "";
7907
+ const path = `/api/work-artifacts/by-entity${qs ? `?${qs}` : ""}`;
7908
+ const data = await client.rawRequest("GET", path);
7909
+ sendJson(res, 200, data);
7910
+ }
7911
+ catch (err) {
7912
+ sendJson(res, 502, { error: safeErrorMessage(err) });
7913
+ }
7914
+ return true;
7915
+ }
7916
+ // Artifact detail: GET /orgx/api/artifacts/:artifactId
7917
+ if (artifactDetailMatch && method === "GET") {
7918
+ try {
7919
+ const artifactId = decodeURIComponent(artifactDetailMatch[1]);
7920
+ const path = `/api/artifacts/${encodeURIComponent(artifactId)}`;
7921
+ const data = await client.rawRequest("GET", path);
7922
+ sendJson(res, 200, data);
7923
+ }
7924
+ catch (err) {
7925
+ const warning = safeErrorMessage(err);
7926
+ const artifactId = decodeURIComponent(artifactDetailMatch[1]);
7927
+ const fallback = buildLocalArtifactDetailFallback(artifactId, warning);
7928
+ if (fallback) {
7929
+ sendJson(res, 200, fallback);
7930
+ }
7931
+ else {
7932
+ sendJson(res, 502, { error: warning });
7933
+ }
7934
+ }
7935
+ return true;
7936
+ }
7123
7937
  // Entity comments route: GET/POST /orgx/api/entities/{type}/{id}/comments
7124
7938
  if (entityCommentsMatch && (method === "GET" || method === "POST")) {
7125
7939
  try {
@@ -7339,7 +8153,15 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
7339
8153
  switch (route) {
7340
8154
  case "agent-suite/status": {
7341
8155
  try {
7342
- const skillPack = await resolveSkillPackOverrides({ client });
8156
+ // Resolve skill pack overrides tolerate failures so the plan
8157
+ // is still returned even when the remote API is unreachable.
8158
+ let skillPack = null;
8159
+ try {
8160
+ skillPack = await resolveSkillPackOverrides({ client });
8161
+ }
8162
+ catch {
8163
+ // Fall through with null — plan will use builtin defaults.
8164
+ }
7343
8165
  const state = readSkillPackState();
7344
8166
  const updateAvailable = Boolean(state.remote?.checksum &&
7345
8167
  state.pack?.checksum &&
@@ -8050,6 +8872,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
8050
8872
  };
8051
8873
  const instance = upsertRuntimeInstanceFromHook(payload);
8052
8874
  broadcastRuntimeSse("runtime.updated", instance);
8875
+ clearSnapshotResponseCache();
8053
8876
  const fallbackPhaseByEvent = {
8054
8877
  session_start: "intent",
8055
8878
  heartbeat: "execution",
@@ -8547,6 +9370,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
8547
9370
  const decisionStatus = searchParams.get("status") ?? "pending";
8548
9371
  const includeIdleRaw = searchParams.get("include_idle");
8549
9372
  const includeIdle = includeIdleRaw === null ? undefined : includeIdleRaw !== "false";
9373
+ const snapshotCacheKey = `${route}?${searchParams.toString()}`;
9374
+ const cachedSnapshot = readSnapshotResponseCache(snapshotCacheKey);
9375
+ if (cachedSnapshot) {
9376
+ sendJson(res, 200, cachedSnapshot);
9377
+ return true;
9378
+ }
8550
9379
  const degraded = [];
8551
9380
  const contextStore = readAgentContexts();
8552
9381
  const agentContexts = contextStore.agents;
@@ -8808,13 +9637,27 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
8808
9637
  sessions = injectRuntimeInstancesAsSessions(sessions, runtimeInstances);
8809
9638
  sessions = enrichSessionsWithRuntime(sessions, runtimeInstances);
8810
9639
  activity = enrichActivityWithRuntime(activity, runtimeInstances);
9640
+ activity = applyAgentContextsToActivity(activity, {
9641
+ agents: agentContexts,
9642
+ runs: runContexts,
9643
+ });
8811
9644
  try {
8812
- appendActivityItems(activity);
9645
+ // Avoid reprocessing/storing large activity snapshots when the leading window
9646
+ // is unchanged and we persisted recently.
9647
+ const fingerprint = snapshotActivityFingerprint(activity);
9648
+ const now = Date.now();
9649
+ const shouldPersist = fingerprint !== lastSnapshotActivityFingerprint ||
9650
+ now - lastSnapshotActivityPersistAt >= SNAPSHOT_ACTIVITY_PERSIST_MIN_INTERVAL_MS;
9651
+ if (shouldPersist) {
9652
+ appendActivityItems(activity);
9653
+ lastSnapshotActivityFingerprint = fingerprint;
9654
+ lastSnapshotActivityPersistAt = now;
9655
+ }
8813
9656
  }
8814
9657
  catch {
8815
9658
  // best effort
8816
9659
  }
8817
- sendJson(res, 200, {
9660
+ const payload = {
8818
9661
  sessions,
8819
9662
  activity,
8820
9663
  handoffs,
@@ -8824,7 +9667,9 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
8824
9667
  outbox: outboxStatus,
8825
9668
  generatedAt: new Date().toISOString(),
8826
9669
  degraded: degraded.length > 0 ? degraded : undefined,
8827
- });
9670
+ };
9671
+ writeSnapshotResponseCache(snapshotCacheKey, payload);
9672
+ sendJson(res, 200, payload);
8828
9673
  return true;
8829
9674
  }
8830
9675
  // Legacy endpoints retained for backwards compatibility.
@@ -8897,6 +9742,16 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
8897
9742
  until,
8898
9743
  cursor,
8899
9744
  });
9745
+ {
9746
+ const ctx = readAgentContexts();
9747
+ page = {
9748
+ ...page,
9749
+ activities: applyAgentContextsToActivity(page.activities, {
9750
+ agents: ctx.agents,
9751
+ runs: ctx.runs ?? {},
9752
+ }),
9753
+ };
9754
+ }
8900
9755
  // If the local store is empty or we haven't warmed enough history yet, opportunistically
8901
9756
  // warm from the remote API (which only supports since+limit) and re-page.
8902
9757
  const warmKey = `${run ?? ""}::${since ?? ""}::${until ?? ""}`;
@@ -8929,6 +9784,16 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
8929
9784
  until,
8930
9785
  cursor,
8931
9786
  });
9787
+ {
9788
+ const ctx = readAgentContexts();
9789
+ page = {
9790
+ ...page,
9791
+ activities: applyAgentContextsToActivity(page.activities, {
9792
+ agents: ctx.agents,
9793
+ runs: ctx.runs ?? {},
9794
+ }),
9795
+ };
9796
+ }
8932
9797
  }
8933
9798
  catch {
8934
9799
  // best effort
@@ -9098,6 +9963,75 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
9098
9963
  }
9099
9964
  return true;
9100
9965
  }
9966
+ case "live/filesystem/open": {
9967
+ if (method !== "GET") {
9968
+ sendJson(res, 405, { error: "Use GET /orgx/api/live/filesystem/open?path=..." });
9969
+ return true;
9970
+ }
9971
+ const rawPath = searchParams.get("path") ?? "";
9972
+ if (!rawPath.trim()) {
9973
+ sendJson(res, 400, { error: "path is required" });
9974
+ return true;
9975
+ }
9976
+ const pathInput = rawPath.trim();
9977
+ if (/^https?:\/\//i.test(pathInput)) {
9978
+ res.writeHead(302, {
9979
+ Location: pathInput,
9980
+ ...SECURITY_HEADERS,
9981
+ ...CORS_HEADERS,
9982
+ });
9983
+ res.end();
9984
+ return true;
9985
+ }
9986
+ const resolvedPath = resolveFilesystemOpenPath(pathInput);
9987
+ const escapedInput = escapeHtml(pathInput);
9988
+ const escapedResolved = escapeHtml(resolvedPath);
9989
+ const shellPath = resolvedPath.replaceAll("'", "'\\''");
9990
+ if (!existsSync(resolvedPath)) {
9991
+ sendHtml(res, 404, `<!doctype html><html><head><meta charset="utf-8"/><title>Path Not Found</title><style>body{margin:0;padding:24px;background:#080808;color:#e5e7eb;font:14px/1.5 ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}pre{background:#0f0f0f;border:1px solid rgba(255,255,255,.08);padding:12px;border-radius:10px;white-space:pre-wrap;word-break:break-word}a{color:#BFFF00}</style></head><body><h1 style="margin:0 0 8px;font-size:18px;">Path not found</h1><p style="margin:0 0 12px;color:#9ca3af;">The evidence path no longer exists.</p><pre>${escapedInput}</pre><p style="margin:12px 0 0;color:#9ca3af;">Resolved as:</p><pre>${escapedResolved}</pre></body></html>`);
9992
+ return true;
9993
+ }
9994
+ try {
9995
+ const stats = statSync(resolvedPath);
9996
+ if (stats.isDirectory()) {
9997
+ const entries = readdirSync(resolvedPath);
9998
+ const visibleEntries = entries.slice(0, FILE_PREVIEW_MAX_DIR_ENTRIES);
9999
+ const items = visibleEntries
10000
+ .map((name) => {
10001
+ const nextPath = resolve(resolvedPath, name);
10002
+ const href = `/orgx/api/live/filesystem/open?path=${encodeURIComponent(nextPath)}`;
10003
+ return `<li style="margin:0 0 6px;"><a href="${href}" target="_blank" rel="noreferrer" style="color:#BFFF00;text-decoration:none;">${escapeHtml(name)}</a></li>`;
10004
+ })
10005
+ .join("");
10006
+ const overflowNote = entries.length > visibleEntries.length
10007
+ ? `<p style="margin:12px 0 0;color:#9ca3af;">Showing ${visibleEntries.length} of ${entries.length} entries.</p>`
10008
+ : "";
10009
+ sendHtml(res, 200, `<!doctype html><html><head><meta charset="utf-8"/><title>Directory Preview</title><style>body{margin:0;padding:24px;background:#080808;color:#e5e7eb;font:14px/1.5 ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}pre,ul{background:#0f0f0f;border:1px solid rgba(255,255,255,.08);padding:12px;border-radius:10px}ul{list-style:none;margin:0;max-height:70vh;overflow:auto}pre{white-space:pre-wrap;word-break:break-word}code{color:#7dd3c0}</style></head><body><h1 style="margin:0 0 8px;font-size:18px;">Directory</h1><p style="margin:0 0 12px;color:#9ca3af;">${escapedResolved}</p><ul>${items || "<li style=\"color:#9ca3af;\">(empty)</li>"}</ul>${overflowNote}<p style="margin:12px 0 0;color:#9ca3af;">Tip: open in terminal with <code>ls -la '${escapeHtml(shellPath)}'</code></p></body></html>`);
10010
+ return true;
10011
+ }
10012
+ if (!stats.isFile()) {
10013
+ sendHtml(res, 200, `<!doctype html><html><head><meta charset="utf-8"/><title>Unsupported Path</title><style>body{margin:0;padding:24px;background:#080808;color:#e5e7eb;font:14px/1.5 ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}pre{background:#0f0f0f;border:1px solid rgba(255,255,255,.08);padding:12px;border-radius:10px;white-space:pre-wrap;word-break:break-word}</style></head><body><h1 style="margin:0 0 8px;font-size:18px;">Unsupported path type</h1><p style="margin:0 0 12px;color:#9ca3af;">Only files and directories are previewable.</p><pre>${escapedResolved}</pre></body></html>`);
10014
+ return true;
10015
+ }
10016
+ const totalBytes = Number.isFinite(stats.size) ? Math.max(0, stats.size) : 0;
10017
+ const { previewBuffer, truncated } = readFilePreview(resolvedPath, totalBytes);
10018
+ const isBinary = previewBuffer.includes(0);
10019
+ const sizeLabel = `${totalBytes.toLocaleString()} bytes`;
10020
+ if (isBinary) {
10021
+ sendHtml(res, 200, `<!doctype html><html><head><meta charset="utf-8"/><title>Binary File</title><style>body{margin:0;padding:24px;background:#080808;color:#e5e7eb;font:14px/1.5 ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}pre{background:#0f0f0f;border:1px solid rgba(255,255,255,.08);padding:12px;border-radius:10px;white-space:pre-wrap;word-break:break-word}code{color:#7dd3c0}</style></head><body><h1 style="margin:0 0 8px;font-size:18px;">Binary file</h1><p style="margin:0 0 12px;color:#9ca3af;">Cannot render binary content in browser preview.</p><pre>${escapedResolved}\n${escapeHtml(sizeLabel)}</pre><p style="margin:12px 0 0;color:#9ca3af;">Inspect in terminal with <code>file '${escapeHtml(shellPath)}'</code></p></body></html>`);
10022
+ return true;
10023
+ }
10024
+ const previewText = previewBuffer.toString("utf8");
10025
+ const truncationNote = truncated
10026
+ ? `<p style="margin:12px 0 0;color:#9ca3af;">Preview truncated to first ${FILE_PREVIEW_MAX_BYTES.toLocaleString()} bytes.</p>`
10027
+ : "";
10028
+ sendHtml(res, 200, `<!doctype html><html><head><meta charset="utf-8"/><title>File Preview</title><style>body{margin:0;padding:24px;background:#080808;color:#e5e7eb;font:14px/1.5 ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}pre{background:#0f0f0f;border:1px solid rgba(255,255,255,.08);padding:12px;border-radius:10px;white-space:pre;overflow:auto;max-height:75vh}code{color:#7dd3c0}</style></head><body><h1 style="margin:0 0 8px;font-size:18px;">File preview</h1><p style="margin:0 0 12px;color:#9ca3af;">${escapedResolved}</p><p style="margin:0 0 12px;color:#9ca3af;">${escapeHtml(sizeLabel)}</p><pre>${escapeHtml(previewText)}</pre>${truncationNote}<p style="margin:12px 0 0;color:#9ca3af;">Open in terminal with <code>cat '${escapeHtml(shellPath)}'</code></p></body></html>`);
10029
+ }
10030
+ catch (err) {
10031
+ sendHtml(res, 500, `<!doctype html><html><head><meta charset="utf-8"/><title>Preview Error</title><style>body{margin:0;padding:24px;background:#080808;color:#e5e7eb;font:14px/1.5 ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}pre{background:#0f0f0f;border:1px solid rgba(255,255,255,.08);padding:12px;border-radius:10px;white-space:pre-wrap;word-break:break-word}</style></head><body><h1 style="margin:0 0 8px;font-size:18px;">Unable to preview path</h1><p style="margin:0 0 12px;color:#9ca3af;">${escapeHtml(safeErrorMessage(err))}</p><pre>${escapedResolved}</pre></body></html>`);
10032
+ }
10033
+ return true;
10034
+ }
9101
10035
  case "live/activity/headline": {
9102
10036
  if (method !== "POST") {
9103
10037
  sendJson(res, 405, { error: "Use POST /orgx/api/live/activity/headline" });
@@ -9628,7 +10562,14 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
9628
10562
  assetPath.startsWith(`${RESOLVED_DIST_ASSETS_DIR}${sep}`);
9629
10563
  }
9630
10564
  if (assetPath && isWithinAssetsDir && existsSync(assetPath)) {
9631
- sendFile(res, assetPath, "public, max-age=31536000, immutable");
10565
+ const assetExt = extname(assetPath).toLowerCase();
10566
+ // JS/CSS chunks can be invalidated by dashboard rebuilds while browsers retain
10567
+ // immutable cached entry chunks in local plugin environments.
10568
+ // Revalidate executable assets to avoid stale chunk graph 404s.
10569
+ const cacheControl = assetExt === ".js" || assetExt === ".css"
10570
+ ? "no-cache"
10571
+ : "public, max-age=31536000, immutable";
10572
+ sendFile(res, assetPath, cacheControl);
9632
10573
  }
9633
10574
  else {
9634
10575
  send404(res);