ltcai 5.5.0 → 6.0.0
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 +43 -24
- package/docs/CHANGELOG.md +69 -0
- package/frontend/openapi.json +716 -3
- package/frontend/src/api/client.ts +119 -2
- package/frontend/src/api/openapi.ts +621 -4
- package/frontend/src/components/FirstRunGuide.tsx +3 -3
- package/frontend/src/features/review/ReviewCard.tsx +91 -0
- package/frontend/src/features/review/ReviewInbox.tsx +112 -0
- package/frontend/src/features/review/reviewHelpers.ts +69 -0
- package/frontend/src/i18n.ts +8 -8
- package/frontend/src/pages/Act.tsx +28 -3
- package/frontend/src/routes.ts +2 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/review_queue.py +162 -0
- package/latticeai/app_factory.py +235 -456
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +86 -1
- package/latticeai/runtime/app_context_runtime.py +13 -0
- package/latticeai/runtime/automation_runtime.py +64 -0
- package/latticeai/runtime/bootstrap.py +48 -0
- package/latticeai/runtime/context_runtime.py +43 -0
- package/latticeai/runtime/hooks_runtime.py +77 -0
- package/latticeai/runtime/lifespan_runtime.py +138 -0
- package/latticeai/runtime/persistence_runtime.py +87 -0
- package/latticeai/runtime/platform_services_runtime.py +39 -0
- package/latticeai/runtime/router_registration.py +570 -0
- package/latticeai/runtime/web_runtime.py +65 -0
- package/latticeai/services/review_queue.py +271 -0
- package/latticeai/services/run_executor.py +33 -0
- package/latticeai/services/triggers.py +30 -1
- package/package.json +1 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/index-D2zafMYb.js +16 -0
- package/static/app/assets/index-D2zafMYb.js.map +1 -0
- package/static/app/assets/index-xRn29gI8.css +2 -0
- package/static/app/index.html +2 -2
- package/static/app/assets/index-C7vzwUjU.js +0 -16
- package/static/app/assets/index-C7vzwUjU.js.map +0 -1
- package/static/app/assets/index-HN4f2wbe.css +0 -2
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Brain review queue (5.6.0) — the suggestion inbox.
|
|
2
|
+
|
|
3
|
+
Automation/trigger runs drop drafts here; the user approves, dismisses, snoozes,
|
|
4
|
+
or unsnoozes them. This service owns the *policy* (legal status transitions,
|
|
5
|
+
snooze expiry semantics, run_now back-linking); the store owns persistence.
|
|
6
|
+
|
|
7
|
+
Design decisions (agreed in #develop-with-openclaw):
|
|
8
|
+
|
|
9
|
+
* **run_now ≠ approve.** "Run now" is a preview/regenerate action: it executes
|
|
10
|
+
the underlying workflow but does **not** change ``status``. The fresh run id
|
|
11
|
+
is back-linked into ``payload.last_run_id`` / ``provenance.run_id`` and
|
|
12
|
+
``updated_at`` is bumped. Accepting the result is a separate ``approve``.
|
|
13
|
+
* **Snooze expiry is read-time only.** A snoozed item whose ``snoozed_until``
|
|
14
|
+
has passed is surfaced with ``effective_status == "pending"``; the stored
|
|
15
|
+
``status`` is left untouched (no scheduler mutation in 5.6.0).
|
|
16
|
+
* **Invalid transitions raise** :class:`InvalidReviewTransition` → HTTP 409.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from typing import Any, Callable, Dict, Optional
|
|
23
|
+
|
|
24
|
+
# status: terminal vs. open. Open items can still be acted on.
|
|
25
|
+
OPEN_STATUSES = {"pending", "snoozed"}
|
|
26
|
+
TERMINAL_STATUSES = {"approved", "dismissed"}
|
|
27
|
+
ALL_STATUSES = OPEN_STATUSES | TERMINAL_STATUSES
|
|
28
|
+
|
|
29
|
+
REVIEW_SOURCES = frozenset({"workflow_run", "trigger", "kg_change_digest"})
|
|
30
|
+
|
|
31
|
+
# Which source statuses each action is allowed from.
|
|
32
|
+
_ALLOWED_FROM: Dict[str, set] = {
|
|
33
|
+
"approve": {"pending", "snoozed"},
|
|
34
|
+
"dismiss": {"pending", "snoozed"},
|
|
35
|
+
"snooze": {"pending", "snoozed"},
|
|
36
|
+
# unsnooze only makes sense from a *stored* snoozed state (not pending).
|
|
37
|
+
"unsnooze": {"snoozed"},
|
|
38
|
+
# run_now is a preview, not a transition — only while still open.
|
|
39
|
+
"run_now": {"pending", "snoozed"},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def review_queue_opted_in(workflow: Dict[str, Any]) -> bool:
|
|
44
|
+
"""True when the workflow's trigger node opts into the review inbox."""
|
|
45
|
+
for node in workflow.get("nodes") or []:
|
|
46
|
+
if node.get("type") != "trigger":
|
|
47
|
+
continue
|
|
48
|
+
if (node.get("config") or {}).get("review_queue") is True:
|
|
49
|
+
return True
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class InvalidReviewTransition(Exception):
|
|
54
|
+
"""Raised when an action is not legal from the item's current status."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, action: str, status: str) -> None:
|
|
57
|
+
self.action = action
|
|
58
|
+
self.status = status
|
|
59
|
+
super().__init__(f"cannot {action!r} a review item in status {status!r}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _parse_iso(value: Optional[str]) -> Optional[datetime]:
|
|
63
|
+
if not value:
|
|
64
|
+
return None
|
|
65
|
+
try:
|
|
66
|
+
return datetime.fromisoformat(str(value))
|
|
67
|
+
except (TypeError, ValueError):
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ReviewQueueService:
|
|
72
|
+
"""Policy layer over the store's workspace-scoped ``review_items``."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, *, store: Any, clock: Callable[[], datetime] = datetime.now) -> None:
|
|
75
|
+
self._store = store
|
|
76
|
+
self._clock = clock
|
|
77
|
+
|
|
78
|
+
# ── creation (also the review_sink entry point) ──────────────────────
|
|
79
|
+
def create(
|
|
80
|
+
self,
|
|
81
|
+
*,
|
|
82
|
+
title: str,
|
|
83
|
+
summary: str = "",
|
|
84
|
+
source: str = "workflow_run",
|
|
85
|
+
kind: str = "suggestion",
|
|
86
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
87
|
+
provenance: Optional[Dict[str, Any]] = None,
|
|
88
|
+
user_email: Optional[str] = None,
|
|
89
|
+
workspace_id: Optional[str] = None,
|
|
90
|
+
) -> Dict[str, Any]:
|
|
91
|
+
if source not in REVIEW_SOURCES:
|
|
92
|
+
raise ValueError(f"source must be one of {sorted(REVIEW_SOURCES)}")
|
|
93
|
+
item = self._store.create_review_item(
|
|
94
|
+
title=title,
|
|
95
|
+
summary=summary,
|
|
96
|
+
source=source,
|
|
97
|
+
kind=kind,
|
|
98
|
+
payload=payload,
|
|
99
|
+
provenance=provenance,
|
|
100
|
+
user_email=user_email,
|
|
101
|
+
workspace_id=workspace_id,
|
|
102
|
+
)
|
|
103
|
+
return self._view(item)
|
|
104
|
+
|
|
105
|
+
def list(
|
|
106
|
+
self, *, workspace_id: Optional[str] = None, user_email: Optional[str] = None,
|
|
107
|
+
status: Optional[str] = None, source: Optional[str] = None,
|
|
108
|
+
) -> Dict[str, Any]:
|
|
109
|
+
items = [
|
|
110
|
+
self._view(it)
|
|
111
|
+
for it in self._store.list_review_items(
|
|
112
|
+
workspace_id=workspace_id, user_email=user_email, source=source,
|
|
113
|
+
)
|
|
114
|
+
]
|
|
115
|
+
if status:
|
|
116
|
+
# Filter on the *effective* status so an expired snooze reads as pending.
|
|
117
|
+
items = [it for it in items if it["effective_status"] == status]
|
|
118
|
+
return {"items": items}
|
|
119
|
+
|
|
120
|
+
def get(self, item_id: str, *, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
121
|
+
return self._view(self._store.get_review_item(item_id, workspace_id=workspace_id))
|
|
122
|
+
|
|
123
|
+
# ── transitions ──────────────────────────────────────────────────────
|
|
124
|
+
def approve(self, item_id: str, *, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
125
|
+
return self._transition(item_id, "approve", "approved", workspace_id=workspace_id)
|
|
126
|
+
|
|
127
|
+
def dismiss(self, item_id: str, *, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
128
|
+
return self._transition(item_id, "dismiss", "dismissed", workspace_id=workspace_id)
|
|
129
|
+
|
|
130
|
+
def snooze(
|
|
131
|
+
self, item_id: str, *, until: str, workspace_id: Optional[str] = None,
|
|
132
|
+
) -> Dict[str, Any]:
|
|
133
|
+
item = self._store.get_review_item(item_id, workspace_id=workspace_id)
|
|
134
|
+
self._guard("snooze", item)
|
|
135
|
+
updated = self._store.update_review_item(
|
|
136
|
+
item_id, workspace_id=workspace_id, status="snoozed", snoozed_until=until,
|
|
137
|
+
)
|
|
138
|
+
return self._view(updated)
|
|
139
|
+
|
|
140
|
+
def unsnooze(self, item_id: str, *, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
141
|
+
"""Return a snoozed item to the pending queue.
|
|
142
|
+
|
|
143
|
+
Only legal from a *stored* ``status == "snoozed"`` (an expired snooze is
|
|
144
|
+
still stored as snoozed, so it remains unsnoozable). Clears the timer and
|
|
145
|
+
sets ``status = "pending"``; any other source status raises 409.
|
|
146
|
+
"""
|
|
147
|
+
item = self._store.get_review_item(item_id, workspace_id=workspace_id)
|
|
148
|
+
self._guard("unsnooze", item)
|
|
149
|
+
updated = self._store.update_review_item(
|
|
150
|
+
item_id, workspace_id=workspace_id, status="pending", snoozed_until=None,
|
|
151
|
+
)
|
|
152
|
+
return self._view(updated)
|
|
153
|
+
|
|
154
|
+
# ── run_now: preview/regenerate, NOT a status change ─────────────────
|
|
155
|
+
def run_now(
|
|
156
|
+
self,
|
|
157
|
+
item_id: str,
|
|
158
|
+
*,
|
|
159
|
+
runner: Callable[[Dict[str, Any]], Any],
|
|
160
|
+
workspace_id: Optional[str] = None,
|
|
161
|
+
) -> Dict[str, Any]:
|
|
162
|
+
"""Execute the item's underlying workflow and back-link the new run.
|
|
163
|
+
|
|
164
|
+
``status`` is intentionally left untouched — only ``payload.last_run_id``,
|
|
165
|
+
``provenance.run_id`` and ``updated_at`` move. ``runner`` receives the raw
|
|
166
|
+
stored item and must return a run id (str) or a dict carrying one.
|
|
167
|
+
"""
|
|
168
|
+
item = self._store.get_review_item(item_id, workspace_id=workspace_id)
|
|
169
|
+
self._guard("run_now", item)
|
|
170
|
+
run_id = _extract_run_id(runner(item))
|
|
171
|
+
payload = dict(item.get("payload") or {})
|
|
172
|
+
provenance = dict(item.get("provenance") or {})
|
|
173
|
+
if run_id is not None:
|
|
174
|
+
payload["last_run_id"] = run_id
|
|
175
|
+
provenance["run_id"] = run_id
|
|
176
|
+
updated = self._store.update_review_item(
|
|
177
|
+
item_id, workspace_id=workspace_id, payload=payload, provenance=provenance,
|
|
178
|
+
)
|
|
179
|
+
return self._view(updated)
|
|
180
|
+
|
|
181
|
+
# ── internals ────────────────────────────────────────────────────────
|
|
182
|
+
def _transition(
|
|
183
|
+
self, item_id: str, action: str, new_status: str, *, workspace_id: Optional[str],
|
|
184
|
+
) -> Dict[str, Any]:
|
|
185
|
+
item = self._store.get_review_item(item_id, workspace_id=workspace_id)
|
|
186
|
+
self._guard(action, item)
|
|
187
|
+
patch: Dict[str, Any] = {"status": new_status}
|
|
188
|
+
# Leaving the snooze state clears the timer so it can't resurface later.
|
|
189
|
+
if item.get("snoozed_until") is not None:
|
|
190
|
+
patch["snoozed_until"] = None
|
|
191
|
+
updated = self._store.update_review_item(item_id, workspace_id=workspace_id, **patch)
|
|
192
|
+
return self._view(updated)
|
|
193
|
+
|
|
194
|
+
def _guard(self, action: str, item: Dict[str, Any]) -> None:
|
|
195
|
+
status = str(item.get("status") or "pending")
|
|
196
|
+
if status not in _ALLOWED_FROM.get(action, set()):
|
|
197
|
+
raise InvalidReviewTransition(action, status)
|
|
198
|
+
|
|
199
|
+
def _effective_status(self, item: Dict[str, Any]) -> str:
|
|
200
|
+
"""Read-time view: an expired snooze reads as pending (no mutation)."""
|
|
201
|
+
if str(item.get("status")) != "snoozed":
|
|
202
|
+
return str(item.get("status") or "pending")
|
|
203
|
+
until = _parse_iso(item.get("snoozed_until"))
|
|
204
|
+
if until is not None and until <= self._clock():
|
|
205
|
+
return "pending"
|
|
206
|
+
return "snoozed"
|
|
207
|
+
|
|
208
|
+
def _view(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
|
209
|
+
view = dict(item)
|
|
210
|
+
view["effective_status"] = self._effective_status(item)
|
|
211
|
+
return view
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def enqueue_from_automation(
|
|
215
|
+
sink: "ReviewQueueService",
|
|
216
|
+
*,
|
|
217
|
+
workflow: Dict[str, Any],
|
|
218
|
+
source: str,
|
|
219
|
+
run_result: Any,
|
|
220
|
+
trigger_info: Optional[Dict[str, Any]] = None,
|
|
221
|
+
user_email: Optional[str] = None,
|
|
222
|
+
workspace_id: Optional[str] = None,
|
|
223
|
+
) -> Optional[Dict[str, Any]]:
|
|
224
|
+
"""Opt-in path: enqueue a review item when the workflow trigger requests it."""
|
|
225
|
+
if source not in REVIEW_SOURCES:
|
|
226
|
+
raise ValueError(f"source must be one of {sorted(REVIEW_SOURCES)}")
|
|
227
|
+
if not review_queue_opted_in(workflow):
|
|
228
|
+
return None
|
|
229
|
+
run_id = _extract_run_id(run_result)
|
|
230
|
+
wf_id = workflow.get("id")
|
|
231
|
+
provenance: Dict[str, Any] = {"workflow_id": wf_id}
|
|
232
|
+
if run_id is not None:
|
|
233
|
+
provenance["run_id"] = run_id
|
|
234
|
+
if trigger_info:
|
|
235
|
+
provenance["trigger_id"] = str(trigger_info.get("type") or "")
|
|
236
|
+
provenance["source_detail"] = str(trigger_info.get("type") or "")
|
|
237
|
+
return sink.create(
|
|
238
|
+
title=str(workflow.get("name") or "Automation suggestion"),
|
|
239
|
+
summary="",
|
|
240
|
+
source=source,
|
|
241
|
+
kind="suggestion",
|
|
242
|
+
payload={"workflow_id": wf_id},
|
|
243
|
+
provenance=provenance,
|
|
244
|
+
user_email=user_email,
|
|
245
|
+
workspace_id=workspace_id,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _extract_run_id(result: Any) -> Optional[str]:
|
|
250
|
+
if result is None:
|
|
251
|
+
return None
|
|
252
|
+
if isinstance(result, str):
|
|
253
|
+
return result
|
|
254
|
+
if isinstance(result, dict):
|
|
255
|
+
if result.get("run_id"):
|
|
256
|
+
return str(result["run_id"])
|
|
257
|
+
run = result.get("run")
|
|
258
|
+
if isinstance(run, dict) and run.get("id"):
|
|
259
|
+
return str(run["id"])
|
|
260
|
+
if result.get("id"):
|
|
261
|
+
return str(result["id"])
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
__all__ = [
|
|
266
|
+
"ReviewQueueService",
|
|
267
|
+
"InvalidReviewTransition",
|
|
268
|
+
"REVIEW_SOURCES",
|
|
269
|
+
"review_queue_opted_in",
|
|
270
|
+
"enqueue_from_automation",
|
|
271
|
+
]
|
|
@@ -46,6 +46,7 @@ class RunExecutor:
|
|
|
46
46
|
workspace_graph: Callable[[], Any],
|
|
47
47
|
append_audit_event: Callable[..., None],
|
|
48
48
|
hooks: Any = None,
|
|
49
|
+
review_sink: Any = None,
|
|
49
50
|
) -> None:
|
|
50
51
|
self.store = store
|
|
51
52
|
self.agent_runtime = agent_runtime
|
|
@@ -53,6 +54,8 @@ class RunExecutor:
|
|
|
53
54
|
self.workspace_graph = workspace_graph
|
|
54
55
|
self.append_audit_event = append_audit_event
|
|
55
56
|
self.hooks = hooks
|
|
57
|
+
# Optional review-queue seam (5.6.0). Default None → no behavior change.
|
|
58
|
+
self.review_sink = review_sink
|
|
56
59
|
self._handles: Dict[str, _RunHandle] = {}
|
|
57
60
|
self._results: Dict[str, Dict[str, Any]] = {}
|
|
58
61
|
|
|
@@ -234,6 +237,12 @@ class RunExecutor:
|
|
|
234
237
|
workflow_id=workflow.get("id"),
|
|
235
238
|
status=result.status,
|
|
236
239
|
)
|
|
240
|
+
self._maybe_enqueue_review(
|
|
241
|
+
workflow,
|
|
242
|
+
run_result={"run": updated, "result": result.as_dict()},
|
|
243
|
+
user_email=user_email,
|
|
244
|
+
workspace_id=handle.scope,
|
|
245
|
+
)
|
|
237
246
|
self._results[run_id] = {"run": updated, "result": result.as_dict()}
|
|
238
247
|
except Exception as exc:
|
|
239
248
|
run = self.store.get_workflow_run(run_id, workspace_id=handle.scope)
|
|
@@ -252,6 +261,30 @@ class RunExecutor:
|
|
|
252
261
|
finally:
|
|
253
262
|
self._handles.pop(run_id, None)
|
|
254
263
|
|
|
264
|
+
def _maybe_enqueue_review(
|
|
265
|
+
self,
|
|
266
|
+
workflow: Dict[str, Any],
|
|
267
|
+
*,
|
|
268
|
+
run_result: Dict[str, Any],
|
|
269
|
+
user_email: Optional[str],
|
|
270
|
+
workspace_id: Optional[str],
|
|
271
|
+
) -> None:
|
|
272
|
+
if self.review_sink is None:
|
|
273
|
+
return
|
|
274
|
+
try:
|
|
275
|
+
from latticeai.services.review_queue import enqueue_from_automation
|
|
276
|
+
|
|
277
|
+
enqueue_from_automation(
|
|
278
|
+
self.review_sink,
|
|
279
|
+
workflow=workflow,
|
|
280
|
+
source="workflow_run",
|
|
281
|
+
run_result=run_result,
|
|
282
|
+
user_email=user_email,
|
|
283
|
+
workspace_id=workspace_id,
|
|
284
|
+
)
|
|
285
|
+
except Exception:
|
|
286
|
+
pass
|
|
287
|
+
|
|
255
288
|
def _execute_workflow_sync(
|
|
256
289
|
self,
|
|
257
290
|
workflow: Dict[str, Any],
|
|
@@ -48,9 +48,13 @@ class TriggerService:
|
|
|
48
48
|
data_dir: Path,
|
|
49
49
|
clock: Callable[[], float] = time.time,
|
|
50
50
|
tick_seconds: float = DEFAULT_TICK_SECONDS,
|
|
51
|
+
review_sink: Optional[Any] = None,
|
|
51
52
|
) -> None:
|
|
52
53
|
self._store = store
|
|
53
54
|
self._run_workflow = run_workflow
|
|
55
|
+
# Optional review-queue seam (5.6.0). When wired, fired runs can drop a
|
|
56
|
+
# reviewable suggestion into the queue. Default None keeps legacy behavior.
|
|
57
|
+
self._review_sink = review_sink
|
|
54
58
|
self._state_file = Path(data_dir) / "triggers_state.json"
|
|
55
59
|
self._clock = clock
|
|
56
60
|
self._tick = float(tick_seconds)
|
|
@@ -250,14 +254,39 @@ class TriggerService:
|
|
|
250
254
|
def _fire(self, workflow_id: str, trigger_info: Dict[str, Any]) -> None:
|
|
251
255
|
def _run():
|
|
252
256
|
try:
|
|
253
|
-
self._run_workflow(workflow_id, {"__trigger__": trigger_info})
|
|
257
|
+
result = self._run_workflow(workflow_id, {"__trigger__": trigger_info})
|
|
254
258
|
self._record_fire_outcome(workflow_id, ok=True)
|
|
259
|
+
self._maybe_enqueue_review(workflow_id, trigger_info, result)
|
|
255
260
|
except Exception as exc:
|
|
256
261
|
logging.warning("trigger run failed for %s: %s", workflow_id, exc)
|
|
257
262
|
self._record_fire_outcome(workflow_id, ok=False, detail=str(exc))
|
|
258
263
|
|
|
259
264
|
threading.Thread(target=_run, name=f"trigger-{workflow_id}", daemon=True).start()
|
|
260
265
|
|
|
266
|
+
def _maybe_enqueue_review(
|
|
267
|
+
self, workflow_id: str, trigger_info: Dict[str, Any], result: Dict[str, Any],
|
|
268
|
+
) -> None:
|
|
269
|
+
if self._review_sink is None:
|
|
270
|
+
return
|
|
271
|
+
try:
|
|
272
|
+
from latticeai.services.review_queue import enqueue_from_automation
|
|
273
|
+
|
|
274
|
+
workflows = list(self._store.load_state().get("workflows") or [])
|
|
275
|
+
workflow = next((wf for wf in workflows if wf.get("id") == workflow_id), None)
|
|
276
|
+
if workflow is None:
|
|
277
|
+
return
|
|
278
|
+
trigger_type = str(trigger_info.get("type") or "")
|
|
279
|
+
source = "kg_change_digest" if trigger_type == "brain_event" else "trigger"
|
|
280
|
+
enqueue_from_automation(
|
|
281
|
+
self._review_sink,
|
|
282
|
+
workflow=workflow,
|
|
283
|
+
source=source,
|
|
284
|
+
run_result=result,
|
|
285
|
+
trigger_info=trigger_info,
|
|
286
|
+
)
|
|
287
|
+
except Exception as exc:
|
|
288
|
+
logging.warning("review_sink enqueue failed for %s: %s", workflow_id, exc)
|
|
289
|
+
|
|
261
290
|
def _record_fire_outcome(self, wf_id: str, *, ok: bool, detail: str = "") -> None:
|
|
262
291
|
"""Track consecutive launch failures for degraded status in describe().
|
|
263
292
|
(Deep execution failures are visible via workflow run records; 여기서는 scheduler fire 자체 실패를 카운트.)
|
package/package.json
CHANGED
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "
|
|
2
|
+
"version": "6.0.0",
|
|
3
3
|
"generated_at": "vite",
|
|
4
4
|
"entrypoints": {
|
|
5
5
|
"app": "/static/app/index.html"
|
|
6
6
|
},
|
|
7
7
|
"assets": {
|
|
8
8
|
"../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
|
|
9
|
-
"index.html": "/static/app/assets/index-
|
|
10
|
-
"assets/index-
|
|
9
|
+
"index.html": "/static/app/assets/index-D2zafMYb.js",
|
|
10
|
+
"assets/index-xRn29gI8.css": "/static/app/assets/index-xRn29gI8.css"
|
|
11
11
|
},
|
|
12
12
|
"vite": {
|
|
13
13
|
"../node_modules/@tauri-apps/api/core.js": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"isDynamicEntry": true
|
|
18
18
|
},
|
|
19
19
|
"index.html": {
|
|
20
|
-
"file": "assets/index-
|
|
20
|
+
"file": "assets/index-D2zafMYb.js",
|
|
21
21
|
"name": "index",
|
|
22
22
|
"src": "index.html",
|
|
23
23
|
"isEntry": true,
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"../node_modules/@tauri-apps/api/core.js"
|
|
26
26
|
],
|
|
27
27
|
"css": [
|
|
28
|
-
"assets/index-
|
|
28
|
+
"assets/index-xRn29gI8.css"
|
|
29
29
|
]
|
|
30
30
|
}
|
|
31
31
|
}
|