Преглед изворни кода

fix: a waiting execution's resume state is not truncated or timed out early

An expiresAt of 0 meant never expires, but the TTL guard required it be
greater than 0 to extend past the seven day log TTL, so a permanent
approval silently aged out after a week. Node outputs were also truncated
through the same log-oriented path used for finished executions, so a
resume would seed itself from a placeholder string instead of the real
data it needs.
fszontagh пре 1 месец
родитељ
комит
b8638cfc67
1 измењених фајлова са 21 додато и 6 уклоњено
  1. 21 6
      src/runner/workflow_engine.cpp

+ 21 - 6
src/runner/workflow_engine.cpp

@@ -140,6 +140,14 @@ nlohmann::json ExecutionResult::toJson() const {
     j["error"] = error;
     j["output"] = truncateLargeValues(final_output);
 
+    // A finished execution's node outputs are a log, so large strings are
+    // truncated. A Waiting execution's node outputs are the resume state a
+    // later resume seeds itself from - truncating them would hand the resume
+    // a literal "[omitted N bytes]" placeholder in place of real data, so
+    // they are kept whole. Do not "restore consistency" here later; that
+    // would silently reintroduce truncated resume data.
+    const bool keep_full_outputs = (status == ExecutionStatus::Waiting);
+
     j["nodeExecutions"] = nlohmann::json::array();
     for (const auto& [id, result] : node_results) {
         nlohmann::json nr;
@@ -149,7 +157,7 @@ nlohmann::json ExecutionResult::toJson() const {
         nr["finishedAt"] = result.finished_at;
         // Note: input is intentionally not stored to avoid data duplication
         // Each node's input can be reconstructed from upstream node outputs + connections
-        nr["output"] = truncateLargeValues(result.output);
+        nr["output"] = keep_full_outputs ? result.output : truncateLargeValues(result.output);
         nr["error"] = result.error;
         nr["retryCount"] = result.retry_count;
         j["nodeExecutions"].push_back(nr);
@@ -1450,13 +1458,20 @@ void WorkflowEngine::storeExecution(const ExecutionResult& result) {
     // A finished execution is a log entry and ages out after a week. One that is
     // waiting for a person is work still owed an answer, and having it expire
     // under them loses the run silently, so it lives until its own deadline
-    // plus a day of slack for a late approver.
+    // plus a day of slack for a late approver. An expiresAt of 0 means "no
+    // deadline", not "no need to extend" - it is given the 30-day ceiling the
+    // approval node caps at, so a permanent approval does not fall back to
+    // the ordinary seven-day log TTL and evaporate with no trace.
     int64_t ttl_ms = 7 * 24 * 60 * 60 * 1000;
-    if (result.status == ExecutionStatus::Waiting && result.pause_expires_at > 0) {
-        const int64_t remaining = result.pause_expires_at - TimeUtils::nowMs();
+    if (result.status == ExecutionStatus::Waiting) {
         const int64_t grace = 24 * 60 * 60 * 1000;
-        if (remaining + grace > ttl_ms) {
-            ttl_ms = remaining + grace;
+        const int64_t never_ttl = 30LL * 24 * 60 * 60 * 1000;
+        int64_t wanted = never_ttl;
+        if (result.pause_expires_at > 0) {
+            wanted = result.pause_expires_at - TimeUtils::nowMs() + grace;
+        }
+        if (wanted > ttl_ms) {
+            ttl_ms = wanted;
         }
     }