metheus-governance-mcp-cli 0.2.271 → 0.2.272
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/cli.mjs +247 -116
- package/lib/runner-data.mjs +6 -0
- package/lib/runner-helpers.mjs +11 -0
- package/lib/selftest-runner-scenarios.mjs +65 -0
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -2669,39 +2669,47 @@ function saveBotRunnerState(nextState) {
|
|
|
2669
2669
|
});
|
|
2670
2670
|
}
|
|
2671
2671
|
|
|
2672
|
-
function normalizeBotRunnerExcludedComments(rawExcluded, nowMs = Date.now()) {
|
|
2673
|
-
const normalized = {};
|
|
2674
|
-
for (const [commentIDRaw, entryRaw] of Object.entries(safeObject(rawExcluded))) {
|
|
2675
|
-
const commentID = String(commentIDRaw || "").trim();
|
|
2676
|
-
if (!commentID) continue;
|
|
2672
|
+
function normalizeBotRunnerExcludedComments(rawExcluded, nowMs = Date.now()) {
|
|
2673
|
+
const normalized = {};
|
|
2674
|
+
for (const [commentIDRaw, entryRaw] of Object.entries(safeObject(rawExcluded))) {
|
|
2675
|
+
const commentID = String(commentIDRaw || "").trim();
|
|
2676
|
+
if (!commentID) continue;
|
|
2677
2677
|
const entry = safeObject(entryRaw);
|
|
2678
2678
|
const excludedAt = firstNonEmptyString([entry.excluded_at, entry.excludedAt, entry.updated_at, entry.updatedAt]);
|
|
2679
2679
|
const excludedAtMs = Date.parse(excludedAt);
|
|
2680
2680
|
if (Number.isFinite(excludedAtMs) && nowMs - excludedAtMs > BOT_RUNNER_EXCLUDED_COMMENT_KEEP_MS) {
|
|
2681
2681
|
continue;
|
|
2682
2682
|
}
|
|
2683
|
-
normalized[commentID] = {
|
|
2684
|
-
comment_id: commentID,
|
|
2685
|
-
project_id: String(entry.project_id || entry.projectID || "").trim(),
|
|
2686
|
-
provider: String(entry.provider || "").trim(),
|
|
2687
|
-
request_key: String(entry.request_key || entry.requestKey || "").trim(),
|
|
2688
|
-
route_key: String(entry.route_key || entry.routeKey || "").trim(),
|
|
2689
|
-
excluded_at: excludedAt || new Date(nowMs).toISOString(),
|
|
2690
|
-
reason_code: String(entry.reason_code || entry.reasonCode || entry.reason || "").trim(),
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2683
|
+
normalized[commentID] = {
|
|
2684
|
+
comment_id: commentID,
|
|
2685
|
+
project_id: String(entry.project_id || entry.projectID || "").trim(),
|
|
2686
|
+
provider: String(entry.provider || "").trim(),
|
|
2687
|
+
request_key: String(entry.request_key || entry.requestKey || "").trim(),
|
|
2688
|
+
route_key: String(entry.route_key || entry.routeKey || "").trim(),
|
|
2689
|
+
excluded_at: excludedAt || new Date(nowMs).toISOString(),
|
|
2690
|
+
reason_code: String(entry.reason_code || entry.reasonCode || entry.reason || "").trim(),
|
|
2691
|
+
decision: String(entry.decision || "").trim().toLowerCase(),
|
|
2692
|
+
action: String(entry.action || "").trim().toLowerCase(),
|
|
2693
|
+
conversation_id: String(entry.conversation_id || entry.conversationId || "").trim(),
|
|
2694
|
+
source_message_id: intFromRawAllowZero(entry.source_message_id || entry.sourceMessageID, 0) || undefined,
|
|
2695
|
+
comment_kind: String(entry.comment_kind || entry.commentKind || "").trim().toLowerCase(),
|
|
2696
|
+
request_status: normalizeRunnerRequestStatus(entry.request_status || entry.requestStatus),
|
|
2697
|
+
source_occurred_at: String(entry.source_occurred_at || entry.sourceOccurredAt || "").trim(),
|
|
2698
|
+
stale_after_at: String(entry.stale_after_at || entry.staleAfterAt || "").trim(),
|
|
2699
|
+
selection_state: normalizeRunnerCommentSelectionState(
|
|
2700
|
+
entry.selection_state || entry.selectionState,
|
|
2701
|
+
"superseded",
|
|
2702
|
+
),
|
|
2703
|
+
context_excluded: entry.context_excluded !== false,
|
|
2704
|
+
closed_reason: String(entry.closed_reason || entry.closedReason || "").trim(),
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
return normalized;
|
|
2700
2708
|
}
|
|
2701
2709
|
|
|
2702
|
-
function normalizeRunnerRequestStatus(rawStatus) {
|
|
2703
|
-
const status = String(rawStatus || "").trim().toLowerCase();
|
|
2704
|
-
return [
|
|
2710
|
+
function normalizeRunnerRequestStatus(rawStatus) {
|
|
2711
|
+
const status = String(rawStatus || "").trim().toLowerCase();
|
|
2712
|
+
return [
|
|
2705
2713
|
"planned",
|
|
2706
2714
|
"claimed",
|
|
2707
2715
|
"running",
|
|
@@ -2710,9 +2718,54 @@ function normalizeRunnerRequestStatus(rawStatus) {
|
|
|
2710
2718
|
"expired",
|
|
2711
2719
|
"loop_closed",
|
|
2712
2720
|
].includes(status)
|
|
2713
|
-
? status
|
|
2714
|
-
: "planned";
|
|
2715
|
-
}
|
|
2721
|
+
? status
|
|
2722
|
+
: "planned";
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
function normalizeRunnerCommentSelectionState(rawState, fallback = "pending") {
|
|
2726
|
+
const normalizedFallback = String(fallback || "").trim().toLowerCase() || "pending";
|
|
2727
|
+
const normalized = String(rawState || "").trim().toLowerCase();
|
|
2728
|
+
return [
|
|
2729
|
+
"pending",
|
|
2730
|
+
"consumed",
|
|
2731
|
+
"closed",
|
|
2732
|
+
"superseded",
|
|
2733
|
+
"stale",
|
|
2734
|
+
].includes(normalized)
|
|
2735
|
+
? normalized
|
|
2736
|
+
: normalizedFallback;
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
function resolveRunnerCommentStateSourceOccurredAt(recordRaw) {
|
|
2740
|
+
const record = safeObject(recordRaw);
|
|
2741
|
+
const parsed = safeObject(record.parsedArchive);
|
|
2742
|
+
return firstNonEmptyString([
|
|
2743
|
+
record.source_occurred_at,
|
|
2744
|
+
record.sourceOccurredAt,
|
|
2745
|
+
parsed.occurredAt,
|
|
2746
|
+
parsed.occurred_at,
|
|
2747
|
+
record.created_at,
|
|
2748
|
+
record.createdAt,
|
|
2749
|
+
record.updated_at,
|
|
2750
|
+
record.updatedAt,
|
|
2751
|
+
]);
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
function resolveRunnerCommentStateStaleAfterAt(recordRaw, maxAgeMs = BOT_RUNNER_PENDING_COMMENT_MAX_AGE_MS) {
|
|
2755
|
+
const explicit = firstNonEmptyString([
|
|
2756
|
+
safeObject(recordRaw).stale_after_at,
|
|
2757
|
+
safeObject(recordRaw).staleAfterAt,
|
|
2758
|
+
]);
|
|
2759
|
+
if (explicit) {
|
|
2760
|
+
return explicit;
|
|
2761
|
+
}
|
|
2762
|
+
const sourceOccurredAt = resolveRunnerCommentStateSourceOccurredAt(recordRaw);
|
|
2763
|
+
const sourceOccurredAtMs = Date.parse(sourceOccurredAt);
|
|
2764
|
+
if (!Number.isFinite(sourceOccurredAtMs) || !(maxAgeMs > 0)) {
|
|
2765
|
+
return "";
|
|
2766
|
+
}
|
|
2767
|
+
return new Date(sourceOccurredAtMs + maxAgeMs).toISOString();
|
|
2768
|
+
}
|
|
2716
2769
|
|
|
2717
2770
|
function isFinalRunnerRequestStatus(rawStatus) {
|
|
2718
2771
|
const status = normalizeRunnerRequestStatus(rawStatus);
|
|
@@ -3245,9 +3298,9 @@ function normalizeBotRunnerRequests(rawRequests, nowMs = Date.now()) {
|
|
|
3245
3298
|
return normalized;
|
|
3246
3299
|
}
|
|
3247
3300
|
|
|
3248
|
-
function normalizeBotRunnerConsumedComments(rawConsumed, nowMs = Date.now()) {
|
|
3249
|
-
const normalized = {};
|
|
3250
|
-
for (const [commentIDRaw, entryRaw] of Object.entries(safeObject(rawConsumed))) {
|
|
3301
|
+
function normalizeBotRunnerConsumedComments(rawConsumed, nowMs = Date.now()) {
|
|
3302
|
+
const normalized = {};
|
|
3303
|
+
for (const [commentIDRaw, entryRaw] of Object.entries(safeObject(rawConsumed))) {
|
|
3251
3304
|
const entry = safeObject(entryRaw);
|
|
3252
3305
|
const commentID = String(entry.comment_id || commentIDRaw || "").trim();
|
|
3253
3306
|
if (!commentID) continue;
|
|
@@ -3257,21 +3310,27 @@ function normalizeBotRunnerConsumedComments(rawConsumed, nowMs = Date.now()) {
|
|
|
3257
3310
|
if (Number.isFinite(consumedAtMs) && nowMs - consumedAtMs > BOT_RUNNER_REQUEST_KEEP_MS) {
|
|
3258
3311
|
continue;
|
|
3259
3312
|
}
|
|
3260
|
-
normalized[ledgerKey] = {
|
|
3261
|
-
ledger_key: ledgerKey,
|
|
3262
|
-
comment_id: commentID,
|
|
3263
|
-
project_id: String(entry.project_id || entry.projectID || "").trim(),
|
|
3264
|
-
provider: String(entry.provider || "").trim(),
|
|
3265
|
-
request_key: String(entry.request_key || entry.requestKey || "").trim(),
|
|
3266
|
-
consumed_at: consumedAt || new Date(nowMs).toISOString(),
|
|
3267
|
-
route_key: String(entry.route_key || entry.routeKey || "").trim(),
|
|
3268
|
-
conversation_id: String(entry.conversation_id || entry.conversationId || "").trim(),
|
|
3269
|
-
source_message_id: intFromRawAllowZero(entry.source_message_id || entry.sourceMessageID, 0) || undefined,
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3313
|
+
normalized[ledgerKey] = {
|
|
3314
|
+
ledger_key: ledgerKey,
|
|
3315
|
+
comment_id: commentID,
|
|
3316
|
+
project_id: String(entry.project_id || entry.projectID || "").trim(),
|
|
3317
|
+
provider: String(entry.provider || "").trim(),
|
|
3318
|
+
request_key: String(entry.request_key || entry.requestKey || "").trim(),
|
|
3319
|
+
consumed_at: consumedAt || new Date(nowMs).toISOString(),
|
|
3320
|
+
route_key: String(entry.route_key || entry.routeKey || "").trim(),
|
|
3321
|
+
conversation_id: String(entry.conversation_id || entry.conversationId || "").trim(),
|
|
3322
|
+
source_message_id: intFromRawAllowZero(entry.source_message_id || entry.sourceMessageID, 0) || undefined,
|
|
3323
|
+
source_occurred_at: String(entry.source_occurred_at || entry.sourceOccurredAt || "").trim(),
|
|
3324
|
+
stale_after_at: String(entry.stale_after_at || entry.staleAfterAt || "").trim(),
|
|
3325
|
+
selection_state: normalizeRunnerCommentSelectionState(
|
|
3326
|
+
entry.selection_state || entry.selectionState,
|
|
3327
|
+
"consumed",
|
|
3328
|
+
),
|
|
3329
|
+
comment_kind: String(entry.comment_kind || entry.commentKind || "").trim().toLowerCase(),
|
|
3330
|
+
request_status: normalizeRunnerRequestStatus(entry.request_status || entry.requestStatus),
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3333
|
+
return normalized;
|
|
3275
3334
|
}
|
|
3276
3335
|
|
|
3277
3336
|
function buildRunnerConsumedCommentLedgerKey(commentIDRaw, routeKeyRaw = "", commentKindRaw = "") {
|
|
@@ -4788,10 +4847,10 @@ function upsertRunnerRequest(state, requestKey, patch = {}) {
|
|
|
4788
4847
|
};
|
|
4789
4848
|
}
|
|
4790
4849
|
|
|
4791
|
-
function upsertRunnerConsumedComment(state, commentIDRaw, patch = {}) {
|
|
4792
|
-
const commentID = String(commentIDRaw || "").trim();
|
|
4793
|
-
const currentState = safeObject(state);
|
|
4794
|
-
const consumedComments = normalizeBotRunnerConsumedComments(currentState.consumedComments || currentState.consumed_comments);
|
|
4850
|
+
function upsertRunnerConsumedComment(state, commentIDRaw, patch = {}) {
|
|
4851
|
+
const commentID = String(commentIDRaw || "").trim();
|
|
4852
|
+
const currentState = safeObject(state);
|
|
4853
|
+
const consumedComments = normalizeBotRunnerConsumedComments(currentState.consumedComments || currentState.consumed_comments);
|
|
4795
4854
|
const ledgerKey = buildRunnerConsumedCommentLedgerKey(commentID, patch.route_key, patch.comment_kind);
|
|
4796
4855
|
const existing = safeObject(consumedComments[ledgerKey]);
|
|
4797
4856
|
const nextEntry = {
|
|
@@ -4799,10 +4858,22 @@ function upsertRunnerConsumedComment(state, commentIDRaw, patch = {}) {
|
|
|
4799
4858
|
...safeObject(patch),
|
|
4800
4859
|
comment_id: commentID,
|
|
4801
4860
|
project_id: String(patch.project_id || existing.project_id || "").trim(),
|
|
4802
|
-
provider: String(patch.provider || existing.provider || "").trim(),
|
|
4803
|
-
consumed_at: new Date().toISOString(),
|
|
4804
|
-
|
|
4805
|
-
|
|
4861
|
+
provider: String(patch.provider || existing.provider || "").trim(),
|
|
4862
|
+
consumed_at: new Date().toISOString(),
|
|
4863
|
+
source_occurred_at: firstNonEmptyString([
|
|
4864
|
+
patch.source_occurred_at,
|
|
4865
|
+
existing.source_occurred_at,
|
|
4866
|
+
]),
|
|
4867
|
+
stale_after_at: firstNonEmptyString([
|
|
4868
|
+
patch.stale_after_at,
|
|
4869
|
+
existing.stale_after_at,
|
|
4870
|
+
]),
|
|
4871
|
+
selection_state: normalizeRunnerCommentSelectionState(
|
|
4872
|
+
patch.selection_state || existing.selection_state,
|
|
4873
|
+
"consumed",
|
|
4874
|
+
),
|
|
4875
|
+
request_status: normalizeRunnerRequestStatus(patch.request_status || existing.request_status),
|
|
4876
|
+
};
|
|
4806
4877
|
consumedComments[ledgerKey] = nextEntry;
|
|
4807
4878
|
return {
|
|
4808
4879
|
consumedComments,
|
|
@@ -5128,16 +5199,19 @@ async function claimRunnerRequestForHumanComment({
|
|
|
5128
5199
|
last_source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
|
|
5129
5200
|
last_source_message_thread_id: intFromRawAllowZero(parsed.messageThreadID, 0) || undefined,
|
|
5130
5201
|
});
|
|
5131
|
-
const { consumedComments: nextConsumedComments } = upsertRunnerConsumedComment(stateForClaim, selectedRecord?.id, {
|
|
5132
|
-
project_id: String(normalizedRoute?.projectID || "").trim(),
|
|
5133
|
-
provider: String(normalizedRoute?.provider || "").trim(),
|
|
5134
|
-
request_key: requestKey,
|
|
5135
|
-
route_key: String(routeKey || "").trim(),
|
|
5136
|
-
conversation_id: resolvedConversationID,
|
|
5137
|
-
source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5202
|
+
const { consumedComments: nextConsumedComments } = upsertRunnerConsumedComment(stateForClaim, selectedRecord?.id, {
|
|
5203
|
+
project_id: String(normalizedRoute?.projectID || "").trim(),
|
|
5204
|
+
provider: String(normalizedRoute?.provider || "").trim(),
|
|
5205
|
+
request_key: requestKey,
|
|
5206
|
+
route_key: String(routeKey || "").trim(),
|
|
5207
|
+
conversation_id: resolvedConversationID,
|
|
5208
|
+
source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
|
|
5209
|
+
source_occurred_at: resolveRunnerCommentStateSourceOccurredAt(selectedRecord),
|
|
5210
|
+
stale_after_at: resolveRunnerCommentStateStaleAfterAt(selectedRecord),
|
|
5211
|
+
selection_state: "consumed",
|
|
5212
|
+
comment_kind: commentKind,
|
|
5213
|
+
request_status: "claimed",
|
|
5214
|
+
});
|
|
5141
5215
|
saveBotRunnerState({
|
|
5142
5216
|
routes: stateForClaim.routes,
|
|
5143
5217
|
sharedInboxes: stateForClaim.sharedInboxes || stateForClaim.shared_inboxes,
|
|
@@ -6561,17 +6635,20 @@ function markRunnerRequestLifecycle({
|
|
|
6561
6635
|
const commentID = String(selectedRecord?.id || "").trim();
|
|
6562
6636
|
let nextConsumedComments = currentState.consumedComments || currentState.consumed_comments;
|
|
6563
6637
|
if (commentID) {
|
|
6564
|
-
({ consumedComments: nextConsumedComments } = upsertRunnerConsumedComment(currentState, commentID, {
|
|
6565
|
-
project_id: String(normalizedRoute?.projectID || "").trim(),
|
|
6566
|
-
provider: String(normalizedRoute?.provider || "").trim(),
|
|
6567
|
-
request_key: key,
|
|
6568
|
-
route_key: String(routeKey || "").trim(),
|
|
6569
|
-
conversation_id: conversationID,
|
|
6570
|
-
source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6638
|
+
({ consumedComments: nextConsumedComments } = upsertRunnerConsumedComment(currentState, commentID, {
|
|
6639
|
+
project_id: String(normalizedRoute?.projectID || "").trim(),
|
|
6640
|
+
provider: String(normalizedRoute?.provider || "").trim(),
|
|
6641
|
+
request_key: key,
|
|
6642
|
+
route_key: String(routeKey || "").trim(),
|
|
6643
|
+
conversation_id: conversationID,
|
|
6644
|
+
source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
|
|
6645
|
+
source_occurred_at: resolveRunnerCommentStateSourceOccurredAt(selectedRecord),
|
|
6646
|
+
stale_after_at: resolveRunnerCommentStateStaleAfterAt(selectedRecord),
|
|
6647
|
+
selection_state: isFinalRunnerRequestStatus(nextStatus) ? "closed" : "consumed",
|
|
6648
|
+
comment_kind: String(parsed.kind || "").trim().toLowerCase(),
|
|
6649
|
+
request_status: nextStatus,
|
|
6650
|
+
}));
|
|
6651
|
+
}
|
|
6575
6652
|
saveBotRunnerState({
|
|
6576
6653
|
routes: currentState.routes,
|
|
6577
6654
|
sharedInboxes: currentState.sharedInboxes || currentState.shared_inboxes,
|
|
@@ -6900,16 +6977,22 @@ function mergeServerRunnerRequestLedgerIntoLocalState(currentState, normalizedRo
|
|
|
6900
6977
|
const commentState = safeObject(commentStateRaw);
|
|
6901
6978
|
const commentID = String(commentState.comment_id || commentState.commentID || "").trim();
|
|
6902
6979
|
if (!commentID) continue;
|
|
6903
|
-
const normalizedCommentState = normalizeBotRunnerConsumedComments({ [commentID]: commentState })[commentID];
|
|
6904
|
-
if (normalizedCommentState) {
|
|
6905
|
-
nextConsumedComments[commentID] = normalizedCommentState;
|
|
6906
|
-
}
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6980
|
+
const normalizedCommentState = normalizeBotRunnerConsumedComments({ [commentID]: commentState })[commentID];
|
|
6981
|
+
if (normalizedCommentState) {
|
|
6982
|
+
nextConsumedComments[commentID] = normalizedCommentState;
|
|
6983
|
+
}
|
|
6984
|
+
const normalizedSelectionState = normalizeRunnerCommentSelectionState(commentState.selection_state, "pending");
|
|
6985
|
+
if (
|
|
6986
|
+
commentState.context_excluded === true
|
|
6987
|
+
|| normalizedSelectionState === "stale"
|
|
6988
|
+
|| normalizedSelectionState === "superseded"
|
|
6989
|
+
|| normalizedSelectionState === "closed"
|
|
6990
|
+
) {
|
|
6991
|
+
nextExcludedComments[commentID] = normalizeBotRunnerExcludedComments({
|
|
6992
|
+
[commentID]: {
|
|
6993
|
+
...commentState,
|
|
6994
|
+
excluded_at: commentState.updated_at || commentState.consumed_at,
|
|
6995
|
+
},
|
|
6913
6996
|
})[commentID];
|
|
6914
6997
|
}
|
|
6915
6998
|
}
|
|
@@ -6967,24 +7050,27 @@ function buildProjectRunnerRequestCommentStatesForSync(state, normalizedRoute) {
|
|
|
6967
7050
|
if (!runnerLedgerEntryMatchesProject(entry, normalizedRoute, requestIndex)) continue;
|
|
6968
7051
|
const commentID = String(entry.comment_id || "").trim();
|
|
6969
7052
|
if (!commentID) continue;
|
|
6970
|
-
commentStateMap.set(commentID, {
|
|
6971
|
-
comment_id: commentID,
|
|
6972
|
-
project_id: projectID,
|
|
6973
|
-
request_key: String(entry.request_key || "").trim(),
|
|
6974
|
-
provider: String(entry.provider || provider).trim(),
|
|
7053
|
+
commentStateMap.set(commentID, {
|
|
7054
|
+
comment_id: commentID,
|
|
7055
|
+
project_id: projectID,
|
|
7056
|
+
request_key: String(entry.request_key || "").trim(),
|
|
7057
|
+
provider: String(entry.provider || provider).trim(),
|
|
6975
7058
|
route_key: String(entry.route_key || "").trim(),
|
|
6976
7059
|
conversation_id: String(entry.conversation_id || "").trim(),
|
|
6977
7060
|
source_message_id: intFromRawAllowZero(entry.source_message_id, 0) || undefined,
|
|
6978
7061
|
comment_kind: String(entry.comment_kind || "").trim(),
|
|
6979
7062
|
request_status: String(entry.request_status || "").trim(),
|
|
6980
|
-
reason_code: "",
|
|
6981
|
-
decision: "",
|
|
6982
|
-
action: "",
|
|
6983
|
-
context_excluded: false,
|
|
6984
|
-
closed_reason: "",
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
7063
|
+
reason_code: "",
|
|
7064
|
+
decision: "",
|
|
7065
|
+
action: "",
|
|
7066
|
+
context_excluded: false,
|
|
7067
|
+
closed_reason: "",
|
|
7068
|
+
source_occurred_at: String(entry.source_occurred_at || "").trim(),
|
|
7069
|
+
stale_after_at: String(entry.stale_after_at || "").trim(),
|
|
7070
|
+
selection_state: normalizeRunnerCommentSelectionState(entry.selection_state, "consumed"),
|
|
7071
|
+
consumed_at: String(entry.consumed_at || "").trim(),
|
|
7072
|
+
});
|
|
7073
|
+
}
|
|
6988
7074
|
|
|
6989
7075
|
for (const entryRaw of Object.values(excludedComments)) {
|
|
6990
7076
|
const entry = safeObject(entryRaw);
|
|
@@ -7004,13 +7090,19 @@ function buildProjectRunnerRequestCommentStatesForSync(state, normalizedRoute) {
|
|
|
7004
7090
|
comment_kind: String(entry.comment_kind || existing.comment_kind || "").trim(),
|
|
7005
7091
|
request_status: String(entry.request_status || existing.request_status || "").trim(),
|
|
7006
7092
|
reason_code: String(entry.reason_code || "").trim(),
|
|
7007
|
-
decision: String(entry.decision || "").trim(),
|
|
7008
|
-
action: String(entry.action || "").trim(),
|
|
7009
|
-
context_excluded: entry.context_excluded !== false,
|
|
7010
|
-
closed_reason: String(entry.closed_reason || existing.closed_reason || "").trim(),
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7093
|
+
decision: String(entry.decision || "").trim(),
|
|
7094
|
+
action: String(entry.action || "").trim(),
|
|
7095
|
+
context_excluded: entry.context_excluded !== false,
|
|
7096
|
+
closed_reason: String(entry.closed_reason || existing.closed_reason || "").trim(),
|
|
7097
|
+
source_occurred_at: String(entry.source_occurred_at || existing.source_occurred_at || "").trim(),
|
|
7098
|
+
stale_after_at: String(entry.stale_after_at || existing.stale_after_at || "").trim(),
|
|
7099
|
+
selection_state: normalizeRunnerCommentSelectionState(
|
|
7100
|
+
entry.selection_state || existing.selection_state,
|
|
7101
|
+
entry.context_excluded !== false ? "superseded" : "pending",
|
|
7102
|
+
),
|
|
7103
|
+
consumed_at: String(existing.consumed_at || entry.excluded_at || "").trim(),
|
|
7104
|
+
});
|
|
7105
|
+
}
|
|
7014
7106
|
|
|
7015
7107
|
return Array.from(commentStateMap.values());
|
|
7016
7108
|
}
|
|
@@ -10258,10 +10350,10 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
|
|
|
10258
10350
|
}
|
|
10259
10351
|
return true;
|
|
10260
10352
|
});
|
|
10261
|
-
const pendingWork = selectRunnerPendingWork({
|
|
10262
|
-
comments: commentsForPending,
|
|
10263
|
-
importOutcome,
|
|
10264
|
-
refreshedState,
|
|
10353
|
+
const pendingWork = selectRunnerPendingWork({
|
|
10354
|
+
comments: commentsForPending,
|
|
10355
|
+
importOutcome,
|
|
10356
|
+
refreshedState,
|
|
10265
10357
|
mode,
|
|
10266
10358
|
parseArchivedChatComment,
|
|
10267
10359
|
pendingSelectionOptions: {
|
|
@@ -10271,13 +10363,52 @@ async function processRunnerRouteOnce(route, runtime, mode, options = {}) {
|
|
|
10271
10363
|
applyPendingAgeSelection,
|
|
10272
10364
|
normalizeArchiveCommentRecord,
|
|
10273
10365
|
},
|
|
10274
|
-
});
|
|
10275
|
-
const pending = pendingWork.pending;
|
|
10276
|
-
if (pending.staleSkippedLatest) {
|
|
10277
|
-
|
|
10278
|
-
|
|
10279
|
-
|
|
10280
|
-
|
|
10366
|
+
});
|
|
10367
|
+
const pending = pendingWork.pending;
|
|
10368
|
+
if (pending.staleSkippedLatest) {
|
|
10369
|
+
let staleStateChanged = false;
|
|
10370
|
+
for (const staleRecordRaw of ensureArray(pending.staleSkipped)) {
|
|
10371
|
+
const staleRecord = safeObject(staleRecordRaw);
|
|
10372
|
+
const commentID = String(staleRecord.id || "").trim();
|
|
10373
|
+
if (!commentID) continue;
|
|
10374
|
+
const parsed = safeObject(staleRecord.parsedArchive);
|
|
10375
|
+
const existingExcludedEntry = safeObject(excludedComments[commentID]);
|
|
10376
|
+
const stalePatch = {
|
|
10377
|
+
project_id: String(normalizedRoute.projectID || "").trim(),
|
|
10378
|
+
provider: String(normalizedRoute.provider || "").trim(),
|
|
10379
|
+
route_key: routeKey,
|
|
10380
|
+
request_key: String(existingExcludedEntry.request_key || "").trim(),
|
|
10381
|
+
conversation_id: String(parsed.conversationID || parsed.conversationId || existingExcludedEntry.conversation_id || "").trim(),
|
|
10382
|
+
source_message_id: intFromRawAllowZero(parsed.messageID || existingExcludedEntry.source_message_id, 0) || undefined,
|
|
10383
|
+
comment_kind: String(parsed.kind || existingExcludedEntry.comment_kind || "").trim().toLowerCase(),
|
|
10384
|
+
request_status: "expired",
|
|
10385
|
+
source_occurred_at: resolveRunnerCommentStateSourceOccurredAt(staleRecord),
|
|
10386
|
+
stale_after_at: resolveRunnerCommentStateStaleAfterAt(staleRecord),
|
|
10387
|
+
selection_state: "stale",
|
|
10388
|
+
reason_code: "stale_archive_message",
|
|
10389
|
+
decision: "skip",
|
|
10390
|
+
action: "stale_skipped",
|
|
10391
|
+
context_excluded: true,
|
|
10392
|
+
closed_reason: "stale_source_message",
|
|
10393
|
+
};
|
|
10394
|
+
const updatedExcludedEntry = markBotRunnerExcludedComment(commentID, stalePatch);
|
|
10395
|
+
if (
|
|
10396
|
+
normalizeRunnerCommentSelectionState(updatedExcludedEntry.selection_state, "superseded") === "stale"
|
|
10397
|
+
&& String(updatedExcludedEntry.closed_reason || "").trim() === "stale_source_message"
|
|
10398
|
+
) {
|
|
10399
|
+
staleStateChanged = true;
|
|
10400
|
+
}
|
|
10401
|
+
}
|
|
10402
|
+
if (staleStateChanged) {
|
|
10403
|
+
await syncRunnerRequestLedgerForProjectToServer({
|
|
10404
|
+
normalizedRoute,
|
|
10405
|
+
runtime,
|
|
10406
|
+
});
|
|
10407
|
+
}
|
|
10408
|
+
saveRunnerRouteState(routeKey, buildRunnerRouteStateFromComment(pending.staleSkippedLatest, {
|
|
10409
|
+
last_reason: `skipped ${ensureArray(pending.staleSkipped).length} stale archive message(s) older than ${Math.floor(BOT_RUNNER_PENDING_COMMENT_MAX_AGE_MS / 60000)} minutes`,
|
|
10410
|
+
}));
|
|
10411
|
+
refreshedState = safeObject(loadBotRunnerState().routes[routeKey]);
|
|
10281
10412
|
}
|
|
10282
10413
|
if (pending.shouldPrime && pending.latest) {
|
|
10283
10414
|
saveRunnerRouteState(routeKey, buildRunnerRouteStateFromComment(pending.latest, { primed: true }));
|
package/lib/runner-data.mjs
CHANGED
|
@@ -478,6 +478,9 @@ export async function listProjectRunnerRequestCommentStates(
|
|
|
478
478
|
action: String(row.action || row.Action || "").trim(),
|
|
479
479
|
context_excluded: row.context_excluded === true || row.ContextExcluded === true,
|
|
480
480
|
closed_reason: String(row.closed_reason || row.closedReason || row.ClosedReason || "").trim(),
|
|
481
|
+
source_occurred_at: String(row.source_occurred_at || row.sourceOccurredAt || row.SourceOccurredAt || "").trim(),
|
|
482
|
+
stale_after_at: String(row.stale_after_at || row.staleAfterAt || row.StaleAfterAt || "").trim(),
|
|
483
|
+
selection_state: String(row.selection_state || row.selectionState || row.SelectionState || "").trim(),
|
|
481
484
|
consumed_at: String(row.consumed_at || row.consumedAt || row.ConsumedAt || "").trim(),
|
|
482
485
|
updated_at: String(row.updated_at || row.updatedAt || row.UpdatedAt || "").trim(),
|
|
483
486
|
};
|
|
@@ -513,6 +516,9 @@ export async function upsertProjectRunnerRequestCommentState(
|
|
|
513
516
|
action: String(raw.action || "").trim(),
|
|
514
517
|
context_excluded: raw.context_excluded === true,
|
|
515
518
|
closed_reason: String(raw.closed_reason || "").trim(),
|
|
519
|
+
source_occurred_at: String(raw.source_occurred_at || "").trim() || undefined,
|
|
520
|
+
stale_after_at: String(raw.stale_after_at || "").trim() || undefined,
|
|
521
|
+
selection_state: String(raw.selection_state || "").trim() || undefined,
|
|
516
522
|
consumed_at: String(raw.consumed_at || "").trim() || undefined,
|
|
517
523
|
};
|
|
518
524
|
const extraHeaders = actorUserID ? { "X-Actor-User-Id": actorUserID } : {};
|
package/lib/runner-helpers.mjs
CHANGED
|
@@ -271,6 +271,10 @@ function isArchiveRecordWithinPendingAgeLimit(record, rawOptions) {
|
|
|
271
271
|
if (!(options.maxPendingAgeMs > 0)) {
|
|
272
272
|
return true;
|
|
273
273
|
}
|
|
274
|
+
const staleAfterTime = Date.parse(String(safeObject(record).staleAfterAt || ""));
|
|
275
|
+
if (Number.isFinite(staleAfterTime)) {
|
|
276
|
+
return options.nowMs <= staleAfterTime;
|
|
277
|
+
}
|
|
274
278
|
const recordTime = Date.parse(resolveArchiveRecordEventTime(record));
|
|
275
279
|
if (!Number.isFinite(recordTime)) {
|
|
276
280
|
return true;
|
|
@@ -541,9 +545,16 @@ export function normalizeArchiveCommentRecord(rawComment, parseArchivedChatComme
|
|
|
541
545
|
updatedAt: firstNonEmptyString([comment.updated_at, comment.updatedAt]),
|
|
542
546
|
authorUserID: firstNonEmptyString([comment.author_user_id, comment.authorUserId, comment.created_by]),
|
|
543
547
|
sourceOccurredAt: firstNonEmptyString([
|
|
548
|
+
comment.source_occurred_at,
|
|
549
|
+
comment.sourceOccurredAt,
|
|
544
550
|
safeObject(parsedArchive).occurredAt,
|
|
545
551
|
safeObject(parsedArchive).occurred_at,
|
|
546
552
|
]),
|
|
553
|
+
staleAfterAt: firstNonEmptyString([
|
|
554
|
+
comment.stale_after_at,
|
|
555
|
+
comment.staleAfterAt,
|
|
556
|
+
]),
|
|
557
|
+
selectionState: String(comment.selection_state || comment.selectionState || "").trim().toLowerCase(),
|
|
547
558
|
parsedArchive,
|
|
548
559
|
};
|
|
549
560
|
}
|
|
@@ -844,6 +844,71 @@ export async function runSelftestRunnerScenarios(push, deps) {
|
|
|
844
844
|
);
|
|
845
845
|
}
|
|
846
846
|
|
|
847
|
+
try {
|
|
848
|
+
const nowMs = Date.parse("2026-03-31T12:00:00.000Z");
|
|
849
|
+
const pendingSelection = selectPendingArchiveComments(
|
|
850
|
+
[
|
|
851
|
+
{
|
|
852
|
+
id: "cursor-comment-persisted-stale",
|
|
853
|
+
createdAt: "2026-03-31T11:55:00.000Z",
|
|
854
|
+
updatedAt: "2026-03-31T11:55:00.000Z",
|
|
855
|
+
parsedArchive: {
|
|
856
|
+
kind: "telegram_message",
|
|
857
|
+
chatID: "-1001",
|
|
858
|
+
messageID: 1148,
|
|
859
|
+
body: "@RyoAI_bot 기준 댓글",
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
id: "stale-by-persisted-cutoff",
|
|
864
|
+
createdAt: "2026-03-31T11:59:58.000Z",
|
|
865
|
+
updatedAt: "2026-03-31T11:59:58.000Z",
|
|
866
|
+
staleAfterAt: "2026-03-31T11:50:00.000Z",
|
|
867
|
+
parsedArchive: {
|
|
868
|
+
kind: "telegram_message",
|
|
869
|
+
chatID: "-1001",
|
|
870
|
+
messageID: 327,
|
|
871
|
+
body: "@RyoAI_bot archive created recently but source already stale",
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
id: "fresh-by-persisted-cutoff",
|
|
876
|
+
createdAt: "2026-03-31T11:59:59.000Z",
|
|
877
|
+
updatedAt: "2026-03-31T11:59:59.000Z",
|
|
878
|
+
staleAfterAt: "2026-03-31T12:05:00.000Z",
|
|
879
|
+
parsedArchive: {
|
|
880
|
+
kind: "telegram_message",
|
|
881
|
+
chatID: "-1001",
|
|
882
|
+
messageID: 1149,
|
|
883
|
+
body: "@RyoAI_bot still fresh by persisted cutoff",
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
],
|
|
887
|
+
{
|
|
888
|
+
last_processed_comment_id: "cursor-comment-persisted-stale",
|
|
889
|
+
},
|
|
890
|
+
"start",
|
|
891
|
+
(record) => record,
|
|
892
|
+
{
|
|
893
|
+
maxPendingAgeMs: 15 * 60 * 1000,
|
|
894
|
+
nowMs,
|
|
895
|
+
},
|
|
896
|
+
);
|
|
897
|
+
push(
|
|
898
|
+
"runner_pending_selection_prefers_persisted_stale_after_at_over_recent_archive_created_at",
|
|
899
|
+
pendingSelection.pending.length === 1
|
|
900
|
+
&& String(pendingSelection.pending[0]?.id || "") === "fresh-by-persisted-cutoff"
|
|
901
|
+
&& ensureArray(pendingSelection.staleSkipped).some((record) => String(record?.id || "") === "stale-by-persisted-cutoff"),
|
|
902
|
+
`pending=${pendingSelection.pending.map((item) => item.id).join(",") || "(none)"} stale=${ensureArray(pendingSelection.staleSkipped).map((item) => item.id).join(",") || "(none)"}`,
|
|
903
|
+
);
|
|
904
|
+
} catch (err) {
|
|
905
|
+
push(
|
|
906
|
+
"runner_pending_selection_prefers_persisted_stale_after_at_over_recent_archive_created_at",
|
|
907
|
+
false,
|
|
908
|
+
String(err?.message || err),
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
847
912
|
try {
|
|
848
913
|
const duplicateComments = [
|
|
849
914
|
{
|