@useorgx/openclaw-plugin 0.7.11 → 0.7.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -122
- package/dashboard/dist/assets/0RUEVzJa.js +1 -0
- package/dashboard/dist/assets/0RUEVzJa.js.br +0 -0
- package/dashboard/dist/assets/0RUEVzJa.js.gz +0 -0
- package/dashboard/dist/assets/3TtV4moZ.js +1 -0
- package/dashboard/dist/assets/3TtV4moZ.js.br +0 -0
- package/dashboard/dist/assets/3TtV4moZ.js.gz +0 -0
- package/dashboard/dist/assets/3VwNyxUf.js +1 -0
- package/dashboard/dist/assets/3VwNyxUf.js.br +0 -0
- package/dashboard/dist/assets/3VwNyxUf.js.gz +0 -0
- package/dashboard/dist/assets/{beHYBbh6.js → 7DhYqBrM.js} +2 -2
- package/dashboard/dist/assets/7DhYqBrM.js.br +0 -0
- package/dashboard/dist/assets/7DhYqBrM.js.gz +0 -0
- package/dashboard/dist/assets/{tcEHYcbW.js → BCudUvwg.js} +1 -1
- package/dashboard/dist/assets/BCudUvwg.js.br +0 -0
- package/dashboard/dist/assets/BCudUvwg.js.gz +0 -0
- package/dashboard/dist/assets/BV0BcV1u.js +53 -0
- package/dashboard/dist/assets/BV0BcV1u.js.br +0 -0
- package/dashboard/dist/assets/BV0BcV1u.js.gz +0 -0
- package/dashboard/dist/assets/BVvffj0x.js +1 -0
- package/dashboard/dist/assets/BVvffj0x.js.br +0 -0
- package/dashboard/dist/assets/BVvffj0x.js.gz +0 -0
- package/dashboard/dist/assets/BiOgVMED.js +1 -0
- package/dashboard/dist/assets/BiOgVMED.js.br +0 -0
- package/dashboard/dist/assets/BiOgVMED.js.gz +0 -0
- package/dashboard/dist/assets/BjK42gtU.js +1 -0
- package/dashboard/dist/assets/BjK42gtU.js.br +0 -0
- package/dashboard/dist/assets/BjK42gtU.js.gz +0 -0
- package/dashboard/dist/assets/C-MOJWHs.js +1 -0
- package/dashboard/dist/assets/C-MOJWHs.js.br +0 -0
- package/dashboard/dist/assets/C-MOJWHs.js.gz +0 -0
- package/dashboard/dist/assets/{CD-q5mdP.js → C91KLKit.js} +1 -1
- package/dashboard/dist/assets/C91KLKit.js.br +0 -0
- package/dashboard/dist/assets/C91KLKit.js.gz +0 -0
- package/dashboard/dist/assets/{AqVoI3SF.js → C9fvfXmS.js} +1 -1
- package/dashboard/dist/assets/C9fvfXmS.js.br +0 -0
- package/dashboard/dist/assets/C9fvfXmS.js.gz +0 -0
- package/dashboard/dist/assets/CFZ4Swr5.js +1 -0
- package/dashboard/dist/assets/CFZ4Swr5.js.br +0 -0
- package/dashboard/dist/assets/CFZ4Swr5.js.gz +0 -0
- package/dashboard/dist/assets/{DCP-C7fn.js → CGj8kRhg.js} +1 -1
- package/dashboard/dist/assets/CGj8kRhg.js.br +0 -0
- package/dashboard/dist/assets/CGj8kRhg.js.gz +0 -0
- package/dashboard/dist/assets/CJjEAGFN.js +1 -0
- package/dashboard/dist/assets/CJjEAGFN.js.br +0 -0
- package/dashboard/dist/assets/CJjEAGFN.js.gz +0 -0
- package/dashboard/dist/assets/CKrH5fYO.js +1 -0
- package/dashboard/dist/assets/CKrH5fYO.js.br +0 -0
- package/dashboard/dist/assets/CKrH5fYO.js.gz +0 -0
- package/dashboard/dist/assets/CMTTPXch.js +1 -0
- package/dashboard/dist/assets/CMTTPXch.js.br +0 -0
- package/dashboard/dist/assets/CMTTPXch.js.gz +0 -0
- package/dashboard/dist/assets/CSlBSRyv.js +1 -0
- package/dashboard/dist/assets/CSlBSRyv.js.br +0 -0
- package/dashboard/dist/assets/CSlBSRyv.js.gz +0 -0
- package/dashboard/dist/assets/CnPC783_.js +1 -0
- package/dashboard/dist/assets/CnPC783_.js.br +0 -0
- package/dashboard/dist/assets/CnPC783_.js.gz +0 -0
- package/dashboard/dist/assets/Ctw95IkC.js +1 -0
- package/dashboard/dist/assets/Ctw95IkC.js.br +0 -0
- package/dashboard/dist/assets/Ctw95IkC.js.gz +0 -0
- package/dashboard/dist/assets/DHz-aQPw.js +1 -0
- package/dashboard/dist/assets/DHz-aQPw.js.br +0 -0
- package/dashboard/dist/assets/DHz-aQPw.js.gz +0 -0
- package/dashboard/dist/assets/DNX2foSJ.css +1 -0
- package/dashboard/dist/assets/DNX2foSJ.css.br +0 -0
- package/dashboard/dist/assets/DNX2foSJ.css.gz +0 -0
- package/dashboard/dist/assets/Dj2k1r16.js +8 -0
- package/dashboard/dist/assets/Dj2k1r16.js.br +0 -0
- package/dashboard/dist/assets/Dj2k1r16.js.gz +0 -0
- package/dashboard/dist/assets/DxUw4FMR.js +212 -0
- package/dashboard/dist/assets/DxUw4FMR.js.br +0 -0
- package/dashboard/dist/assets/DxUw4FMR.js.gz +0 -0
- package/dashboard/dist/assets/T2NFtzAv.js +1 -0
- package/dashboard/dist/assets/T2NFtzAv.js.br +0 -0
- package/dashboard/dist/assets/T2NFtzAv.js.gz +0 -0
- package/dashboard/dist/assets/cX2e-TLi.js +1 -0
- package/dashboard/dist/assets/cX2e-TLi.js.br +0 -0
- package/dashboard/dist/assets/cX2e-TLi.js.gz +0 -0
- package/dashboard/dist/assets/eeHXe_OQ.js +9 -0
- package/dashboard/dist/assets/eeHXe_OQ.js.br +0 -0
- package/dashboard/dist/assets/eeHXe_OQ.js.gz +0 -0
- package/dashboard/dist/assets/{Du1wfrXa.js → gZr_xKlA.js} +2 -2
- package/dashboard/dist/assets/gZr_xKlA.js.br +0 -0
- package/dashboard/dist/assets/gZr_xKlA.js.gz +0 -0
- package/dashboard/dist/brand/control-tower.png +0 -0
- package/dashboard/dist/brand/design-codex.png +0 -0
- package/dashboard/dist/brand/engineering-autopilot.png +0 -0
- package/dashboard/dist/brand/launch-captain.png +0 -0
- package/dashboard/dist/brand/orgx-logo.png +0 -0
- package/dashboard/dist/brand/pipeline-intelligence.png +0 -0
- package/dashboard/dist/brand/product-orchestrator.png +0 -0
- package/dashboard/dist/brand/xandy-orchestrator.png +0 -0
- package/dashboard/dist/index.html +8 -6
- package/dashboard/dist/index.html.br +0 -0
- package/dashboard/dist/index.html.gz +0 -0
- package/dist/hash-utils.d.ts +1 -0
- package/dist/hash-utils.js +4 -0
- package/dist/http/helpers/auto-continue-engine.js +124 -67
- package/dist/http/helpers/autopilot-slice-utils.js +112 -66
- package/dist/http/helpers/hash-utils.d.ts +1 -1
- package/dist/http/helpers/hash-utils.js +1 -1
- package/dist/http/helpers/mission-control.js +5 -2
- package/dist/http/helpers/queue-constants.d.ts +37 -0
- package/dist/http/helpers/queue-constants.js +34 -0
- package/dist/http/helpers/slice-experience-v2.js +2 -5
- package/dist/http/helpers/slice-run-projections.js +2 -5
- package/dist/http/helpers/workspace-scope.js +4 -3
- package/dist/http/index.js +101 -60
- package/dist/http/routes/chat.js +1 -21
- package/dist/http/routes/live-snapshot.js +4 -23
- package/dist/http/routes/mission-control-actions.js +2 -18
- package/dist/http/routes/mission-control-read.js +4 -107
- package/dist/lib/type-coercion.d.ts +10 -0
- package/dist/lib/type-coercion.js +82 -0
- package/dist/mcp-http-handler.js +14 -2
- package/dist/openclaw.plugin.json +1 -1
- package/dist/services/experiment-randomization.js +9 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -2
- package/dashboard/dist/assets/AqVoI3SF.js.br +0 -0
- package/dashboard/dist/assets/AqVoI3SF.js.gz +0 -0
- package/dashboard/dist/assets/BC4WvnHJ.js +0 -1
- package/dashboard/dist/assets/BC4WvnHJ.js.br +0 -0
- package/dashboard/dist/assets/BC4WvnHJ.js.gz +0 -0
- package/dashboard/dist/assets/BG5mwTkg.js +0 -1
- package/dashboard/dist/assets/BG5mwTkg.js.br +0 -0
- package/dashboard/dist/assets/BG5mwTkg.js.gz +0 -0
- package/dashboard/dist/assets/BJgZIVUQ.js +0 -53
- package/dashboard/dist/assets/BJgZIVUQ.js.br +0 -0
- package/dashboard/dist/assets/BJgZIVUQ.js.gz +0 -0
- package/dashboard/dist/assets/BNh-XYPV.js +0 -1
- package/dashboard/dist/assets/BNh-XYPV.js.br +0 -0
- package/dashboard/dist/assets/BNh-XYPV.js.gz +0 -0
- package/dashboard/dist/assets/BTAEErUY.js +0 -1
- package/dashboard/dist/assets/BTAEErUY.js.br +0 -0
- package/dashboard/dist/assets/BTAEErUY.js.gz +0 -0
- package/dashboard/dist/assets/BepW_590.js +0 -1
- package/dashboard/dist/assets/BepW_590.js.br +0 -0
- package/dashboard/dist/assets/BepW_590.js.gz +0 -0
- package/dashboard/dist/assets/BerAfzjq.js +0 -1
- package/dashboard/dist/assets/BerAfzjq.js.br +0 -0
- package/dashboard/dist/assets/BerAfzjq.js.gz +0 -0
- package/dashboard/dist/assets/Bp3N-QL5.js +0 -212
- package/dashboard/dist/assets/Bp3N-QL5.js.br +0 -0
- package/dashboard/dist/assets/Bp3N-QL5.js.gz +0 -0
- package/dashboard/dist/assets/C-KIc3Wc.js +0 -1
- package/dashboard/dist/assets/C-KIc3Wc.js.br +0 -0
- package/dashboard/dist/assets/C-KIc3Wc.js.gz +0 -0
- package/dashboard/dist/assets/C3dZRz9P.css +0 -1
- package/dashboard/dist/assets/C3dZRz9P.css.br +0 -0
- package/dashboard/dist/assets/C3dZRz9P.css.gz +0 -0
- package/dashboard/dist/assets/CD-q5mdP.js.br +0 -0
- package/dashboard/dist/assets/CD-q5mdP.js.gz +0 -0
- package/dashboard/dist/assets/CL_wXqR7.js +0 -1
- package/dashboard/dist/assets/CL_wXqR7.js.br +0 -0
- package/dashboard/dist/assets/CL_wXqR7.js.gz +0 -0
- package/dashboard/dist/assets/CdvjC9G9.js +0 -1
- package/dashboard/dist/assets/CdvjC9G9.js.br +0 -0
- package/dashboard/dist/assets/CdvjC9G9.js.gz +0 -0
- package/dashboard/dist/assets/Ck2agw-s.js +0 -1
- package/dashboard/dist/assets/Ck2agw-s.js.br +0 -0
- package/dashboard/dist/assets/Ck2agw-s.js.gz +0 -0
- package/dashboard/dist/assets/CxQ08qFN.js +0 -9
- package/dashboard/dist/assets/CxQ08qFN.js.br +0 -0
- package/dashboard/dist/assets/CxQ08qFN.js.gz +0 -0
- package/dashboard/dist/assets/D2CH1H6k.js +0 -1
- package/dashboard/dist/assets/D2CH1H6k.js.br +0 -0
- package/dashboard/dist/assets/D2CH1H6k.js.gz +0 -0
- package/dashboard/dist/assets/D9esz7jd.js +0 -1
- package/dashboard/dist/assets/D9esz7jd.js.br +0 -0
- package/dashboard/dist/assets/D9esz7jd.js.gz +0 -0
- package/dashboard/dist/assets/DCP-C7fn.js.br +0 -0
- package/dashboard/dist/assets/DCP-C7fn.js.gz +0 -0
- package/dashboard/dist/assets/DJASCd69.js +0 -1
- package/dashboard/dist/assets/DJASCd69.js.br +0 -0
- package/dashboard/dist/assets/DJASCd69.js.gz +0 -0
- package/dashboard/dist/assets/Dm9AybAp.js +0 -1
- package/dashboard/dist/assets/Dm9AybAp.js.br +0 -0
- package/dashboard/dist/assets/Dm9AybAp.js.gz +0 -0
- package/dashboard/dist/assets/Du1wfrXa.js.br +0 -0
- package/dashboard/dist/assets/Du1wfrXa.js.gz +0 -0
- package/dashboard/dist/assets/beHYBbh6.js.br +0 -0
- package/dashboard/dist/assets/beHYBbh6.js.gz +0 -0
- package/dashboard/dist/assets/cNrhgGc1.js +0 -8
- package/dashboard/dist/assets/cNrhgGc1.js.br +0 -0
- package/dashboard/dist/assets/cNrhgGc1.js.gz +0 -0
- package/dashboard/dist/assets/tcEHYcbW.js.br +0 -0
- package/dashboard/dist/assets/tcEHYcbW.js.gz +0 -0
package/dist/http/index.js
CHANGED
|
@@ -46,10 +46,11 @@ import { summarizeActivityHeadline } from "./helpers/activity-headline.js";
|
|
|
46
46
|
import { createAutoContinueEngine, } from "./helpers/auto-continue-engine.js";
|
|
47
47
|
import { createAutopilotOperations, } from "./helpers/autopilot-operations.js";
|
|
48
48
|
import { mapDecisionEntity } from "./helpers/decision-mapper.js";
|
|
49
|
-
import { idempotencyKey, stableHash } from "./helpers/hash-utils.js";
|
|
49
|
+
import { deterministicActivityId, idempotencyKey, stableHash } from "./helpers/hash-utils.js";
|
|
50
50
|
import { createCodexBinResolver, } from "./helpers/autopilot-slice-utils.js";
|
|
51
51
|
import { createLocalArtifactDetailFallbackBuilder } from "./helpers/artifact-fallback.js";
|
|
52
52
|
import { buildMissionControlGraph, deriveExecutionPolicy, dedupeStrings, isDispatchableWorkstreamStatus, isDoneStatus, isInProgressStatus, isTodoStatus, listEntitiesSafe, normalizeEntityMutationPayload, pickStringArray, resolveAutoAssignments, selectSliceTasksByScope, } from "./helpers/mission-control.js";
|
|
53
|
+
import { QueueState, LaneState, RunStatus, RunnerSource, } from "./helpers/queue-constants.js";
|
|
53
54
|
import { configureOpenClawProviderRouting, fetchBillingStatusSafe, isPidAlive, listOpenClawAgents, listOpenClawProviderModels, modelImpliesByok, normalizeOpenClawProvider, resolveAutoOpenClawProvider, resolveByokEnvOverrides, spawnOpenClawAgentTurn, stopDetachedProcess, } from "./helpers/openclaw-cli.js";
|
|
54
55
|
import { fetchKickoffContextSafe, renderKickoffMessage } from "./helpers/kickoff-context.js";
|
|
55
56
|
import { createDispatchLifecycle } from "./helpers/dispatch-lifecycle.js";
|
|
@@ -177,6 +178,9 @@ function safeErrorMessage(err) {
|
|
|
177
178
|
if (normalized.includes("failed to fetch") || normalized.includes("network")) {
|
|
178
179
|
return "network request failed";
|
|
179
180
|
}
|
|
181
|
+
if (normalized.includes("relation does not exist")) {
|
|
182
|
+
return sanitized;
|
|
183
|
+
}
|
|
180
184
|
if (normalized.includes("internal_error") || normalized.includes("internal server error")) {
|
|
181
185
|
return "temporary server issue";
|
|
182
186
|
}
|
|
@@ -380,6 +384,16 @@ function deriveStructuredActivityBucket(input) {
|
|
|
380
384
|
"nonBlockingDecisionCount",
|
|
381
385
|
]) ?? 0;
|
|
382
386
|
if (event === "autopilot_slice_result") {
|
|
387
|
+
const parsedStatus = typeof metadata?.parsed_status === "string"
|
|
388
|
+
? metadata.parsed_status.trim().toLowerCase()
|
|
389
|
+
: typeof metadata?.parsedStatus === "string"
|
|
390
|
+
? metadata.parsedStatus.trim().toLowerCase()
|
|
391
|
+
: "";
|
|
392
|
+
if (parsedStatus === "completed" && decisionRequired === false) {
|
|
393
|
+
if (artifacts > 0)
|
|
394
|
+
return "artifact";
|
|
395
|
+
return "message";
|
|
396
|
+
}
|
|
383
397
|
// Any blocked slice result needs decision-first surfacing in the Activity UX.
|
|
384
398
|
if (input.phase === "blocked")
|
|
385
399
|
return "decision";
|
|
@@ -712,26 +726,10 @@ function activityMetadataStr(metadata, keys) {
|
|
|
712
726
|
}
|
|
713
727
|
return null;
|
|
714
728
|
}
|
|
715
|
-
const SEMANTIC_ACTIVITY_EVENTS = new Set([
|
|
716
|
-
"autopilot_slice_result",
|
|
717
|
-
"auto_continue_started",
|
|
718
|
-
"auto_continue_stopped",
|
|
719
|
-
"next_up_manual_dispatch_started",
|
|
720
|
-
"autopilot_slice_mcp_handshake_failed",
|
|
721
|
-
"autopilot_slice_timeout",
|
|
722
|
-
"autopilot_slice_log_stall",
|
|
723
|
-
"auto_continue_spawn_guard_blocked",
|
|
724
|
-
"auto_continue_spawn_guard_rate_limited",
|
|
725
|
-
"autopilot_autofix_scheduled",
|
|
726
|
-
"autopilot_autofix_executed",
|
|
727
|
-
"autopilot_autofix_skipped",
|
|
728
|
-
]);
|
|
729
729
|
function semanticActivityKey(item) {
|
|
730
730
|
const metadata = asActivityMetadataRecord(item.metadata);
|
|
731
731
|
const eventRaw = metadata?.event;
|
|
732
732
|
const event = typeof eventRaw === "string" ? eventRaw.trim().toLowerCase() : "";
|
|
733
|
-
if (!event || !SEMANTIC_ACTIVITY_EVENTS.has(event))
|
|
734
|
-
return null;
|
|
735
733
|
const runLike = (typeof item.runId === "string" && item.runId.trim().length > 0
|
|
736
734
|
? item.runId.trim()
|
|
737
735
|
: null) ??
|
|
@@ -755,6 +753,8 @@ function semanticActivityKey(item) {
|
|
|
755
753
|
const stopReason = activityMetadataStr(metadata, ["stop_reason", "stopReason"]);
|
|
756
754
|
const parsedStatus = activityMetadataStr(metadata, ["parsed_status", "parsedStatus"]);
|
|
757
755
|
const title = (item.title ?? "").trim().toLowerCase();
|
|
756
|
+
if (!event && !runLike && !correlationId)
|
|
757
|
+
return null;
|
|
758
758
|
if (!runLike && !correlationId && !workstreamId && !taskId)
|
|
759
759
|
return null;
|
|
760
760
|
return [
|
|
@@ -769,6 +769,27 @@ function semanticActivityKey(item) {
|
|
|
769
769
|
title,
|
|
770
770
|
].join("|");
|
|
771
771
|
}
|
|
772
|
+
function activityRichness(item) {
|
|
773
|
+
let score = 0;
|
|
774
|
+
const meta = item.metadata;
|
|
775
|
+
if (meta)
|
|
776
|
+
score += Object.keys(meta).length;
|
|
777
|
+
if (item.summary)
|
|
778
|
+
score += 2;
|
|
779
|
+
if (item.description)
|
|
780
|
+
score += 2;
|
|
781
|
+
if (item.agentName)
|
|
782
|
+
score += 1;
|
|
783
|
+
if (item.runtimeLabel)
|
|
784
|
+
score += 1;
|
|
785
|
+
if (item.runtimeProvider)
|
|
786
|
+
score += 1;
|
|
787
|
+
if (item.phase)
|
|
788
|
+
score += 1;
|
|
789
|
+
if (item.state)
|
|
790
|
+
score += 1;
|
|
791
|
+
return score;
|
|
792
|
+
}
|
|
772
793
|
function mergeActivities(base, extra, limit) {
|
|
773
794
|
const merged = [...(base ?? []), ...(extra ?? [])].sort((a, b) => {
|
|
774
795
|
const timestampDelta = Date.parse(b.timestamp) - Date.parse(a.timestamp);
|
|
@@ -779,15 +800,28 @@ function mergeActivities(base, extra, limit) {
|
|
|
779
800
|
const deduped = [];
|
|
780
801
|
const seenIds = new Set();
|
|
781
802
|
const seenSemantic = new Set();
|
|
803
|
+
const semanticIdx = new Map();
|
|
782
804
|
for (const item of merged) {
|
|
783
|
-
if (seenIds.has(item.id))
|
|
805
|
+
if (seenIds.has(item.id)) {
|
|
806
|
+
const existingIdx = deduped.findIndex(d => d.id === item.id);
|
|
807
|
+
if (existingIdx >= 0 && activityRichness(item) > activityRichness(deduped[existingIdx])) {
|
|
808
|
+
deduped[existingIdx] = item;
|
|
809
|
+
}
|
|
784
810
|
continue;
|
|
811
|
+
}
|
|
785
812
|
seenIds.add(item.id);
|
|
786
813
|
const sk = semanticActivityKey(item);
|
|
787
|
-
if (sk && seenSemantic.has(sk))
|
|
814
|
+
if (sk && seenSemantic.has(sk)) {
|
|
815
|
+
const existingIdx = semanticIdx.get(sk);
|
|
816
|
+
if (activityRichness(item) > activityRichness(deduped[existingIdx])) {
|
|
817
|
+
deduped[existingIdx] = item;
|
|
818
|
+
}
|
|
788
819
|
continue;
|
|
789
|
-
|
|
820
|
+
}
|
|
821
|
+
if (sk) {
|
|
790
822
|
seenSemantic.add(sk);
|
|
823
|
+
semanticIdx.set(sk, deduped.length);
|
|
824
|
+
}
|
|
791
825
|
deduped.push(item);
|
|
792
826
|
if (deduped.length >= limit)
|
|
793
827
|
break;
|
|
@@ -2173,11 +2207,11 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2173
2207
|
initiativeStatusById.set(initiativeId, override.status);
|
|
2174
2208
|
}
|
|
2175
2209
|
const queueRank = (state) => {
|
|
2176
|
-
if (state ===
|
|
2210
|
+
if (state === QueueState.RUNNING)
|
|
2177
2211
|
return 0;
|
|
2178
|
-
if (state ===
|
|
2212
|
+
if (state === QueueState.QUEUED)
|
|
2179
2213
|
return 1;
|
|
2180
|
-
if (state ===
|
|
2214
|
+
if (state === QueueState.BLOCKED)
|
|
2181
2215
|
return 2;
|
|
2182
2216
|
return 3;
|
|
2183
2217
|
};
|
|
@@ -2322,12 +2356,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2322
2356
|
const hasRunning = statusValues.some((status) => isInProgressStatus(status));
|
|
2323
2357
|
const hasQueued = statusValues.some((status) => status === "queued" || status === "pending");
|
|
2324
2358
|
const queueState = hasRunning
|
|
2325
|
-
?
|
|
2359
|
+
? QueueState.RUNNING
|
|
2326
2360
|
: hasBlocked
|
|
2327
|
-
?
|
|
2361
|
+
? QueueState.BLOCKED
|
|
2328
2362
|
: hasQueued
|
|
2329
|
-
?
|
|
2330
|
-
:
|
|
2363
|
+
? QueueState.QUEUED
|
|
2364
|
+
: QueueState.IDLE;
|
|
2331
2365
|
const latestRunner = [];
|
|
2332
2366
|
const latestRunnerSeen = new Set();
|
|
2333
2367
|
pushRunnerAgent(latestRunner, latestRunnerSeen, {
|
|
@@ -2341,7 +2375,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2341
2375
|
const runnerAgentId = primaryRunner?.id ?? "unassigned";
|
|
2342
2376
|
const runnerAgentName = primaryRunner?.name ?? "Unassigned";
|
|
2343
2377
|
const pinKey = `${entry.initiativeId}:${entry.workstreamId}`;
|
|
2344
|
-
if (isSuppressed(entry.initiativeId, entry.workstreamId) && queueState !==
|
|
2378
|
+
if (isSuppressed(entry.initiativeId, entry.workstreamId) && queueState !== QueueState.RUNNING) {
|
|
2345
2379
|
continue;
|
|
2346
2380
|
}
|
|
2347
2381
|
fallbackItems.push({
|
|
@@ -2360,7 +2394,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2360
2394
|
runnerAgentId,
|
|
2361
2395
|
runnerAgentName,
|
|
2362
2396
|
runnerAgents,
|
|
2363
|
-
runnerSource:
|
|
2397
|
+
runnerSource: RunnerSource.FALLBACK,
|
|
2364
2398
|
queueState,
|
|
2365
2399
|
blockReason: hasBlocked
|
|
2366
2400
|
? entry.blockers[0] ??
|
|
@@ -2545,7 +2579,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2545
2579
|
: [];
|
|
2546
2580
|
const runScopedToCurrentWorkstream = scopedAllowedWorkstreams.length === 1 &&
|
|
2547
2581
|
scopedAllowedWorkstreams[0] === workstream.id &&
|
|
2548
|
-
autoContinueRun?.status ===
|
|
2582
|
+
autoContinueRun?.status === RunStatus.RUNNING;
|
|
2549
2583
|
const activeRunIds = Array.isArray(autoContinueRun?.activeSliceRunIds)
|
|
2550
2584
|
? autoContinueRun.activeSliceRunIds
|
|
2551
2585
|
.filter((id) => typeof id === "string" && id.trim().length > 0)
|
|
@@ -2595,20 +2629,26 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2595
2629
|
const sliceMilestoneId = defaultScope === "milestone"
|
|
2596
2630
|
? scopeSelection.milestoneIds[0] ?? pin?.preferredMilestoneId ?? null
|
|
2597
2631
|
: null;
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2632
|
+
// Only mark as "running" when the lane is actively executing a slice.
|
|
2633
|
+
// runScopedToCurrentWorkstream means the initiative run targets this
|
|
2634
|
+
// workstream, but the lane may be idle between slices — don't conflate
|
|
2635
|
+
// "scoped to a running initiative" with "actively executing".
|
|
2636
|
+
const laneIsActivelyRunning = laneState === LaneState.RUNNING ||
|
|
2637
|
+
(runScopedToCurrentWorkstream && activeRunIds.length > 0);
|
|
2638
|
+
let queueState = laneIsActivelyRunning
|
|
2639
|
+
? QueueState.RUNNING
|
|
2640
|
+
: candidateTask
|
|
2641
|
+
? QueueState.QUEUED
|
|
2642
|
+
: runScopedToCurrentWorkstream
|
|
2643
|
+
? QueueState.QUEUED
|
|
2644
|
+
: QueueState.IDLE;
|
|
2605
2645
|
let blockReason = null;
|
|
2606
|
-
if (laneState ===
|
|
2607
|
-
queueState =
|
|
2646
|
+
if (laneState === LaneState.BLOCKED) {
|
|
2647
|
+
queueState = QueueState.BLOCKED;
|
|
2608
2648
|
blockReason = autoContinueLane?.blockedReason ?? "Blocked";
|
|
2609
2649
|
}
|
|
2610
|
-
else if (laneState ===
|
|
2611
|
-
queueState =
|
|
2650
|
+
else if (laneState === LaneState.WAITING_DEPENDENCY) {
|
|
2651
|
+
queueState = QueueState.BLOCKED;
|
|
2612
2652
|
if (Array.isArray(autoContinueLane?.waitingOnWorkstreamIds) &&
|
|
2613
2653
|
autoContinueLane.waitingOnWorkstreamIds.length > 0) {
|
|
2614
2654
|
const waitingTitles = autoContinueLane.waitingOnWorkstreamIds
|
|
@@ -2626,12 +2666,12 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2626
2666
|
blockReason = "Waiting on dependency workstreams";
|
|
2627
2667
|
}
|
|
2628
2668
|
}
|
|
2629
|
-
else if (laneState ===
|
|
2630
|
-
queueState =
|
|
2669
|
+
else if (laneState === LaneState.RATE_LIMITED) {
|
|
2670
|
+
queueState = QueueState.BLOCKED;
|
|
2631
2671
|
blockReason = autoContinueLane?.blockedReason ?? "Rate-limited";
|
|
2632
2672
|
}
|
|
2633
2673
|
if (!autoContinueRun && !readyTask && candidateTask) {
|
|
2634
|
-
queueState =
|
|
2674
|
+
queueState = QueueState.BLOCKED;
|
|
2635
2675
|
const blockedDeps = candidateTask.dependencyIds
|
|
2636
2676
|
.map((depId) => nodeById.get(depId))
|
|
2637
2677
|
.filter((dependency) => Boolean(dependency && !isDoneStatus(dependency.status)))
|
|
@@ -2649,7 +2689,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2649
2689
|
if (!candidateTask && !autoContinueRun && !pin) {
|
|
2650
2690
|
continue;
|
|
2651
2691
|
}
|
|
2652
|
-
if (isSuppressed(initiativeId, workstream.id) && queueState !==
|
|
2692
|
+
if (isSuppressed(initiativeId, workstream.id) && queueState !== QueueState.RUNNING) {
|
|
2653
2693
|
continue;
|
|
2654
2694
|
}
|
|
2655
2695
|
runningWorkstreams.add(workstream.id);
|
|
@@ -2684,10 +2724,10 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2684
2724
|
}
|
|
2685
2725
|
const runnerAgents = assignedRunnerAgents.length > 0 ? assignedRunnerAgents : inferredRunnerAgents;
|
|
2686
2726
|
const runnerSource = assignedRunnerAgents.length > 0
|
|
2687
|
-
?
|
|
2727
|
+
? RunnerSource.ASSIGNED
|
|
2688
2728
|
: runnerAgents.length > 0
|
|
2689
|
-
?
|
|
2690
|
-
:
|
|
2729
|
+
? RunnerSource.INFERRED
|
|
2730
|
+
: RunnerSource.FALLBACK;
|
|
2691
2731
|
const primaryRunner = runnerAgents[0] ?? null;
|
|
2692
2732
|
const runnerAgentId = primaryRunner?.id ?? "unassigned";
|
|
2693
2733
|
const runnerAgentName = primaryRunner?.name ?? "Unassigned";
|
|
@@ -2781,14 +2821,14 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2781
2821
|
: activeTaskId
|
|
2782
2822
|
? [activeTaskId]
|
|
2783
2823
|
: [];
|
|
2784
|
-
const queueState = laneState ===
|
|
2785
|
-
?
|
|
2786
|
-
: laneState ===
|
|
2787
|
-
laneState ===
|
|
2788
|
-
laneState ===
|
|
2789
|
-
?
|
|
2790
|
-
:
|
|
2791
|
-
if (isSuppressed(initiativeId, workstream.id) && queueState !==
|
|
2824
|
+
const queueState = laneState === LaneState.RUNNING
|
|
2825
|
+
? QueueState.RUNNING
|
|
2826
|
+
: laneState === LaneState.BLOCKED ||
|
|
2827
|
+
laneState === LaneState.WAITING_DEPENDENCY ||
|
|
2828
|
+
laneState === LaneState.RATE_LIMITED
|
|
2829
|
+
? QueueState.BLOCKED
|
|
2830
|
+
: QueueState.QUEUED;
|
|
2831
|
+
if (isSuppressed(initiativeId, workstream.id) && queueState !== QueueState.RUNNING) {
|
|
2792
2832
|
continue;
|
|
2793
2833
|
}
|
|
2794
2834
|
const runRunnerAgents = [];
|
|
@@ -2814,9 +2854,9 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
2814
2854
|
runnerAgentId: runPrimaryRunner?.id ?? "unassigned",
|
|
2815
2855
|
runnerAgentName: runPrimaryRunner?.name ?? "Unassigned",
|
|
2816
2856
|
runnerAgents: runRunnerAgents,
|
|
2817
|
-
runnerSource: runPrimaryRunner ?
|
|
2857
|
+
runnerSource: runPrimaryRunner ? RunnerSource.INFERRED : RunnerSource.FALLBACK,
|
|
2818
2858
|
queueState,
|
|
2819
|
-
blockReason: queueState ===
|
|
2859
|
+
blockReason: queueState === QueueState.BLOCKED
|
|
2820
2860
|
? lane?.blockedReason ?? "Blocked"
|
|
2821
2861
|
: null,
|
|
2822
2862
|
isPinned: Boolean(pinnedByKey.get(`${initiativeId}:${workstream.id}`)),
|
|
@@ -3240,10 +3280,11 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
3240
3280
|
broadcastRuntimeSse("runtime.updated", runtimeRecord);
|
|
3241
3281
|
}
|
|
3242
3282
|
clearSnapshotResponseCache();
|
|
3283
|
+
const activityId = deterministicActivityId("run_completed", normalizedRunId, nowIso, runtimeRecord?.agentId ?? existingRun?.agentId ?? null, "dashboard_run_mark_completed");
|
|
3243
3284
|
try {
|
|
3244
3285
|
appendActivityItems([
|
|
3245
3286
|
{
|
|
3246
|
-
id:
|
|
3287
|
+
id: activityId,
|
|
3247
3288
|
type: "run_completed",
|
|
3248
3289
|
title: message,
|
|
3249
3290
|
description: reason,
|
|
@@ -3275,7 +3316,7 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
|
|
|
3275
3316
|
try {
|
|
3276
3317
|
const outboxSessionId = runtimeRecord?.initiativeId ?? existingRun?.initiativeId ?? normalizedRunId;
|
|
3277
3318
|
const outboxActivityItem = {
|
|
3278
|
-
id:
|
|
3319
|
+
id: activityId,
|
|
3279
3320
|
type: "run_completed",
|
|
3280
3321
|
title: message,
|
|
3281
3322
|
description: reason,
|
package/dist/http/routes/chat.js
CHANGED
|
@@ -1,25 +1,5 @@
|
|
|
1
1
|
import { getChatThread, linkChatThreadScope, listChatThreads, recordChatLaunch, sendChatMessage, updateChatLaunchStatus, } from "../../chat-store.js";
|
|
2
|
-
|
|
3
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
4
|
-
return null;
|
|
5
|
-
return value;
|
|
6
|
-
}
|
|
7
|
-
function asStringArray(value) {
|
|
8
|
-
if (!Array.isArray(value))
|
|
9
|
-
return [];
|
|
10
|
-
const out = [];
|
|
11
|
-
const seen = new Set();
|
|
12
|
-
for (const entry of value) {
|
|
13
|
-
if (typeof entry !== "string")
|
|
14
|
-
continue;
|
|
15
|
-
const trimmed = entry.trim();
|
|
16
|
-
if (!trimmed || seen.has(trimmed))
|
|
17
|
-
continue;
|
|
18
|
-
seen.add(trimmed);
|
|
19
|
-
out.push(trimmed);
|
|
20
|
-
}
|
|
21
|
-
return out;
|
|
22
|
-
}
|
|
2
|
+
import { asRecord, asStringArray } from "../../lib/type-coercion.js";
|
|
23
3
|
function normalizeStatus(value) {
|
|
24
4
|
const normalized = (value ?? "").trim().toLowerCase();
|
|
25
5
|
if (normalized === "requested" ||
|
|
@@ -450,29 +450,7 @@ export function registerLiveSnapshotRoutes(router, deps) {
|
|
|
450
450
|
try {
|
|
451
451
|
const buffered = await deps.readOutboxItems();
|
|
452
452
|
if (buffered.length > 0) {
|
|
453
|
-
|
|
454
|
-
.sort((a, b) => {
|
|
455
|
-
const d = Date.parse(b.timestamp) - Date.parse(a.timestamp);
|
|
456
|
-
if (d !== 0)
|
|
457
|
-
return d;
|
|
458
|
-
return b.id.localeCompare(a.id);
|
|
459
|
-
})
|
|
460
|
-
.slice(0, activityLimit);
|
|
461
|
-
const deduped = [];
|
|
462
|
-
const seenIds = new Set();
|
|
463
|
-
const seenSemantic = new Set();
|
|
464
|
-
for (const item of merged) {
|
|
465
|
-
if (seenIds.has(item.id))
|
|
466
|
-
continue;
|
|
467
|
-
seenIds.add(item.id);
|
|
468
|
-
const sk = deps.semanticActivityKey(item);
|
|
469
|
-
if (sk && seenSemantic.has(sk))
|
|
470
|
-
continue;
|
|
471
|
-
if (sk)
|
|
472
|
-
seenSemantic.add(sk);
|
|
473
|
-
deduped.push(item);
|
|
474
|
-
}
|
|
475
|
-
activity = deduped;
|
|
453
|
+
activity = deps.mergeActivities(activity, buffered, activityLimit);
|
|
476
454
|
}
|
|
477
455
|
}
|
|
478
456
|
catch (err) {
|
|
@@ -500,6 +478,9 @@ export function registerLiveSnapshotRoutes(router, deps) {
|
|
|
500
478
|
agents: agentContexts,
|
|
501
479
|
runs: runContexts,
|
|
502
480
|
});
|
|
481
|
+
// Final dedup pass after enrichment — catches items that became
|
|
482
|
+
// semantically identical after runtime/agent context decoration.
|
|
483
|
+
activity = deps.mergeActivities(activity, [], activityLimit);
|
|
503
484
|
activity = activity.filter((item) => !shouldHideActivityItem(item));
|
|
504
485
|
sessions = normalizeReportingBlockedSessions({
|
|
505
486
|
sessions,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { resolveWorkspaceScope as resolveCanonicalWorkspaceScope } from "../helpers/workspace-scope.js";
|
|
3
|
+
import { asRecord, asString, asStringArray } from "../../lib/type-coercion.js";
|
|
3
4
|
import { buildDispatchGatewayEnvelope } from "./dispatch-gateway-envelope.js";
|
|
4
5
|
const PLAY_QUEUE_LOOKUP_TIMEOUT_MS = (() => {
|
|
5
6
|
const raw = process.env.ORGX_PLAY_QUEUE_LOOKUP_TIMEOUT_MS;
|
|
@@ -194,24 +195,7 @@ function shouldResetTaskStatus(status, states) {
|
|
|
194
195
|
}
|
|
195
196
|
return false;
|
|
196
197
|
}
|
|
197
|
-
|
|
198
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
199
|
-
return null;
|
|
200
|
-
return value;
|
|
201
|
-
}
|
|
202
|
-
function asString(value) {
|
|
203
|
-
if (typeof value !== "string")
|
|
204
|
-
return null;
|
|
205
|
-
const trimmed = value.trim();
|
|
206
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
207
|
-
}
|
|
208
|
-
function asStringArray(value) {
|
|
209
|
-
if (!Array.isArray(value))
|
|
210
|
-
return [];
|
|
211
|
-
return value
|
|
212
|
-
.map((entry) => asString(entry))
|
|
213
|
-
.filter((entry) => Boolean(entry));
|
|
214
|
-
}
|
|
198
|
+
// asRecord, asString, asStringArray imported from ../../lib/type-coercion.js
|
|
215
199
|
function parseCycleGraphNodes(graph) {
|
|
216
200
|
const root = asRecord(graph);
|
|
217
201
|
const rawNodes = Array.isArray(root?.nodes) ? root.nodes : [];
|
|
@@ -1,34 +1,7 @@
|
|
|
1
1
|
import { listBuiltInSentinels } from "../helpers/sentinel-catalog.js";
|
|
2
2
|
import { resolveWorkspaceScope, workspaceScopeFromHeaders, } from "../helpers/workspace-scope.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
return null;
|
|
6
|
-
return value;
|
|
7
|
-
}
|
|
8
|
-
function asString(value) {
|
|
9
|
-
if (typeof value !== "string")
|
|
10
|
-
return null;
|
|
11
|
-
const trimmed = value.trim();
|
|
12
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
13
|
-
}
|
|
14
|
-
function asArray(value) {
|
|
15
|
-
if (Array.isArray(value))
|
|
16
|
-
return value;
|
|
17
|
-
if (typeof value !== "string")
|
|
18
|
-
return [];
|
|
19
|
-
const trimmed = value.trim();
|
|
20
|
-
try {
|
|
21
|
-
const parsed = JSON.parse(trimmed);
|
|
22
|
-
if (Array.isArray(parsed))
|
|
23
|
-
return parsed;
|
|
24
|
-
if (parsed && typeof parsed === "object")
|
|
25
|
-
return [parsed];
|
|
26
|
-
return [];
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return [];
|
|
30
|
-
}
|
|
31
|
-
}
|
|
3
|
+
import { asRecord, asString, asNumber, asArray, asStringArray, } from "../../lib/type-coercion.js";
|
|
4
|
+
// asRecord, asString, asArray, asNumber, asStringArray imported from ../../lib/type-coercion.js
|
|
32
5
|
function normalizeRunnerValue(value) {
|
|
33
6
|
const raw = asString(value);
|
|
34
7
|
if (!raw)
|
|
@@ -105,29 +78,7 @@ function mergeRunnerAgents(...groups) {
|
|
|
105
78
|
}
|
|
106
79
|
return output;
|
|
107
80
|
}
|
|
108
|
-
|
|
109
|
-
if (typeof value === "number" && Number.isFinite(value))
|
|
110
|
-
return value;
|
|
111
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
112
|
-
const parsed = Number(value);
|
|
113
|
-
if (Number.isFinite(parsed))
|
|
114
|
-
return parsed;
|
|
115
|
-
}
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
function asStringArray(value) {
|
|
119
|
-
const entries = asArray(value);
|
|
120
|
-
if (entries.length === 0)
|
|
121
|
-
return [];
|
|
122
|
-
const values = [];
|
|
123
|
-
for (const entry of entries) {
|
|
124
|
-
const normalized = asString(entry);
|
|
125
|
-
if (!normalized)
|
|
126
|
-
continue;
|
|
127
|
-
values.push(normalized);
|
|
128
|
-
}
|
|
129
|
-
return dedupeStrings(values);
|
|
130
|
-
}
|
|
81
|
+
// asNumber, asStringArray imported from ../../lib/type-coercion.js
|
|
131
82
|
function isCanonicalAllScopeMismatch(canonicalRecord, useAllScope) {
|
|
132
83
|
if (!useAllScope)
|
|
133
84
|
return false;
|
|
@@ -846,64 +797,10 @@ export function registerMissionControlReadRoutes(router, deps) {
|
|
|
846
797
|
sendRouteError(res, 400, "mission-control.read.auto-continue.status.validation", workspaceScope.error);
|
|
847
798
|
return;
|
|
848
799
|
}
|
|
849
|
-
const scopedProjectId = workspaceScope.workspaceId;
|
|
850
800
|
const initiativeId = query.get("initiative_id") ?? query.get("initiativeId") ?? "";
|
|
851
801
|
const id = initiativeId.trim();
|
|
852
|
-
let scopedInitiatives = null;
|
|
853
|
-
if (scopedProjectId) {
|
|
854
|
-
try {
|
|
855
|
-
const ids = await deps.listInitiativeIdsForProject({ projectId: scopedProjectId });
|
|
856
|
-
scopedInitiatives = new Set(ids);
|
|
857
|
-
}
|
|
858
|
-
catch {
|
|
859
|
-
// best effort: if scope lookup is unavailable, fall back to unscoped run resolution.
|
|
860
|
-
scopedInitiatives = null;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
const statusRank = (value) => {
|
|
864
|
-
const normalized = (value ?? "").trim().toLowerCase();
|
|
865
|
-
if (normalized === "running")
|
|
866
|
-
return 0;
|
|
867
|
-
if (normalized === "stopping")
|
|
868
|
-
return 1;
|
|
869
|
-
if (normalized === "blocked")
|
|
870
|
-
return 2;
|
|
871
|
-
if (normalized === "stopped")
|
|
872
|
-
return 3;
|
|
873
|
-
return 4;
|
|
874
|
-
};
|
|
875
|
-
const updatedEpoch = (value) => {
|
|
876
|
-
const parsed = Date.parse(value ?? "");
|
|
877
|
-
return Number.isFinite(parsed) ? parsed : 0;
|
|
878
|
-
};
|
|
879
802
|
if (!id) {
|
|
880
|
-
|
|
881
|
-
const runInitiativeId = (run.initiativeId ?? "").trim();
|
|
882
|
-
if (!runInitiativeId)
|
|
883
|
-
return false;
|
|
884
|
-
if (scopedInitiatives && !scopedInitiatives.has(runInitiativeId))
|
|
885
|
-
return false;
|
|
886
|
-
return true;
|
|
887
|
-
});
|
|
888
|
-
scopedRuns.sort((left, right) => {
|
|
889
|
-
const statusDelta = statusRank(left.status) - statusRank(right.status);
|
|
890
|
-
if (statusDelta !== 0)
|
|
891
|
-
return statusDelta;
|
|
892
|
-
return updatedEpoch(right.updatedAt) - updatedEpoch(left.updatedAt);
|
|
893
|
-
});
|
|
894
|
-
const run = scopedRuns[0] ?? null;
|
|
895
|
-
deps.sendJson(res, 200, {
|
|
896
|
-
ok: true,
|
|
897
|
-
initiativeId: run?.initiativeId ?? null,
|
|
898
|
-
run,
|
|
899
|
-
defaults: {
|
|
900
|
-
tokenBudget: deps.defaultAutoContinueTokenBudget(),
|
|
901
|
-
maxParallelSlices: typeof deps.defaultAutoContinueMaxParallelSlices === "function"
|
|
902
|
-
? deps.defaultAutoContinueMaxParallelSlices()
|
|
903
|
-
: 1,
|
|
904
|
-
tickMs: deps.autoContinueTickMs,
|
|
905
|
-
},
|
|
906
|
-
});
|
|
803
|
+
sendRouteError(res, 400, "mission-control.read.auto-continue.status.validation", "initiativeId is required");
|
|
907
804
|
return;
|
|
908
805
|
}
|
|
909
806
|
const run = deps.autoContinueRuns.get(id) ?? null;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Safely cast unknown to a Record, or null if not a plain object. */
|
|
2
|
+
export declare function asRecord(value: unknown): Record<string, unknown> | null;
|
|
3
|
+
/** Safely cast unknown to a trimmed non-empty string, or null. */
|
|
4
|
+
export declare function asString(value: unknown): string | null;
|
|
5
|
+
/** Safely cast unknown to a finite number, or null. */
|
|
6
|
+
export declare function asNumber(value: unknown): number | null;
|
|
7
|
+
/** Safely cast unknown to an array. Attempts JSON parse for string values. */
|
|
8
|
+
export declare function asArray(value: unknown): unknown[];
|
|
9
|
+
/** Extract a deduplicated string array from unknown. */
|
|
10
|
+
export declare function asStringArray(value: unknown): string[];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Shared type-coercion helpers for safe parsing of unknown API data.
|
|
3
|
+
// Previously duplicated across 20+ files.
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
/** Safely cast unknown to a Record, or null if not a plain object. */
|
|
6
|
+
export function asRecord(value) {
|
|
7
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
8
|
+
return null;
|
|
9
|
+
const proto = Object.getPrototypeOf(value);
|
|
10
|
+
if (proto !== Object.prototype && proto !== null)
|
|
11
|
+
return null;
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
/** Safely cast unknown to a trimmed non-empty string, or null. */
|
|
15
|
+
export function asString(value) {
|
|
16
|
+
if (typeof value !== "string")
|
|
17
|
+
return null;
|
|
18
|
+
const trimmed = value.trim();
|
|
19
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
20
|
+
}
|
|
21
|
+
/** Safely cast unknown to a finite number, or null. */
|
|
22
|
+
export function asNumber(value) {
|
|
23
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
24
|
+
return value;
|
|
25
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
26
|
+
const parsed = Number(value);
|
|
27
|
+
if (Number.isFinite(parsed))
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
/** Safely cast unknown to an array. Attempts JSON parse for string values. */
|
|
33
|
+
export function asArray(value) {
|
|
34
|
+
if (Array.isArray(value))
|
|
35
|
+
return value;
|
|
36
|
+
if (asRecord(value))
|
|
37
|
+
return [value];
|
|
38
|
+
if (typeof value !== "string")
|
|
39
|
+
return [];
|
|
40
|
+
const trimmed = value.trim();
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(trimmed);
|
|
43
|
+
if (Array.isArray(parsed))
|
|
44
|
+
return parsed;
|
|
45
|
+
if (asRecord(parsed))
|
|
46
|
+
return [parsed];
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Extract a deduplicated string array from unknown. */
|
|
54
|
+
export function asStringArray(value) {
|
|
55
|
+
const entries = asArray(value);
|
|
56
|
+
if (entries.length === 0) {
|
|
57
|
+
const scalar = asString(value);
|
|
58
|
+
if (!scalar)
|
|
59
|
+
return [];
|
|
60
|
+
const fallbackEntries = scalar.includes(",") ? scalar.split(",") : [scalar];
|
|
61
|
+
const seenFallback = new Set();
|
|
62
|
+
const fallbackValues = [];
|
|
63
|
+
for (const entry of fallbackEntries) {
|
|
64
|
+
const normalized = asString(entry);
|
|
65
|
+
if (!normalized || seenFallback.has(normalized))
|
|
66
|
+
continue;
|
|
67
|
+
seenFallback.add(normalized);
|
|
68
|
+
fallbackValues.push(normalized);
|
|
69
|
+
}
|
|
70
|
+
return fallbackValues;
|
|
71
|
+
}
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
const values = [];
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
const normalized = asString(entry);
|
|
76
|
+
if (!normalized || seen.has(normalized))
|
|
77
|
+
continue;
|
|
78
|
+
seen.add(normalized);
|
|
79
|
+
values.push(normalized);
|
|
80
|
+
}
|
|
81
|
+
return values;
|
|
82
|
+
}
|