create-workframe 0.1.14 → 0.1.16
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 +1 -1
- package/bin/create-workframe.js +4 -0
- package/package.json +1 -1
- package/scripts/sync-canonical-to-package.mjs +3 -0
- package/workframe-api/activity_feed.py +53 -5
- package/workframe-api/api_errors.py +126 -0
- package/workframe-api/chat_bind.py +8 -1
- package/workframe-api/chat_stream.py +70 -26
- package/workframe-api/crew_registry.py +3 -6
- package/workframe-api/google_auth.py +20 -12
- package/workframe-api/handler_modules/handler_auth.py +66 -21
- package/workframe-api/handler_modules/handler_chat.py +25 -0
- package/workframe-api/handler_modules/handler_install.py +45 -0
- package/workframe-api/handler_modules/handler_provider.py +1 -0
- package/workframe-api/handler_modules/handler_workspace.py +30 -16
- package/workframe-api/hermes_admin.py +9 -2
- package/workframe-api/hermes_profiles.py +25 -5
- package/workframe-api/install_api.py +38 -4
- package/workframe-api/lane_bindings.py +24 -0
- package/workframe-api/llm_error_glossary.py +58 -4
- package/workframe-api/mention_invoke.py +89 -27
- package/workframe-api/model_surface.py +1 -1
- package/workframe-api/provider_bindings.py +44 -2
- package/workframe-api/provider_bootstrap.py +9 -0
- package/workframe-api/provider_catalog.py +3 -3
- package/workframe-api/rooms.py +3 -0
- package/workframe-api/route_registry.py +1 -0
- package/workframe-api/run_ledger.py +52 -8
- package/workframe-api/runtime_cohort.py +11 -1
- package/workframe-api/server.py +25 -1
- package/workframe-api/stack_config.py +4 -1
- package/workframe-api/test_api_errors.py +33 -0
- package/workframe-api/test_enrich_room_messages.py +72 -0
- package/workframe-api/test_exception_hygiene.py +4 -2
- package/workframe-api/test_hermes_admin.py +7 -0
- package/workframe-api/test_install_wizard_resume.py +39 -13
- package/workframe-api/test_model_surface_consistency.py +118 -100
- package/workframe-api/test_profile_model_yaml.py +4 -1
- package/workframe-api/test_provider_bindings.py +10 -0
- package/workframe-api/test_run_ledger.py +2 -1
- package/workframe-api/test_server_reexports.py +4 -0
- package/workframe-api/test_user_llm_prefs.py +99 -0
- package/workframe-api/time-overlay-split.py +58 -0
- package/workframe-api/time-ttft-quick.py +135 -0
- package/workframe-api/time-ttft.py +250 -0
- package/workframe-api/time-wf-stream.py +145 -0
- package/workframe-api/turn_overlay.py +96 -23
- package/workframe-api/updates.py +1 -2
- package/workframe-api/user_prefs.py +26 -5
- package/workframe-api/workspace_bootstrap.py +16 -10
- package/workframe-ui/public/assets/{arc-BT9iZUSd.js → arc-esSBm9C_.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-j-DiBdGu.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-CaEDd3np.js → architectureDiagram-3BPJPVTR-DVVz9RGn.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-DHJhMrIS.js → blockDiagram-GPEHLZMM-B6i10GMX.js} +1 -1
- package/workframe-ui/public/assets/brave-B885JYnH.svg +11 -0
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-Cdyz9hIV.js → c4Diagram-AAUBKEIU-BqwJ7JoH.js} +1 -1
- package/workframe-ui/public/assets/channel-CJ9pFXbx.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-BbZx76LX.js → chunk-2J33WTMH-CyhcedSa.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-DcSxjTIP.js → chunk-3OPIFGDE-1Rr6WuvI.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-B7F-kTY1.js → chunk-4BX2VUAB-D2bRrARV.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-BwwEiRTK.js → chunk-55IACEB6-CKXeZxSr.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-DAi_NksX.js → chunk-5ZQYHXKU-DYQxxPZ4.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-BIaF4XCN.js → chunk-727SXJPM-CGpIgqvD.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-DjYSk1dG.js → chunk-AQP2D5EJ-B88NGQ7d.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-_twnyfgV.js → chunk-BSJP7CBP-CCymiI0O.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-CkNmdy5s.js → chunk-CSCIHK7Q-DRgey03j.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-CjLqd-gl.js → chunk-FMBD7UC4-l1adn8gM.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-_6oNqFjj.js → chunk-KSCS5N6A-CaEvDRPK.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-CeO9Am_D.js → chunk-L5ZTLDWV-BgCnvPTv.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-sRM4YeIe.js → chunk-LZXEDZCA-t2zokHpn.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-CH8tMb21.js → chunk-ND2GUHAM-C3c9p0sv.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-DAI1bsII.js → chunk-NZK2D7GU-DSujF-R8.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-B2e-5a2G.js → chunk-O5CBEL6O-DJJTbM6p.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-Bxk3enxo.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-DdbkX1rg.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-DM2-0q37.js → chunk-XPW4576I-D2Cxn3AG.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-bwCRiiUl.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-bwCRiiUl.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-BnbbewLv.js → cose-bilkent-S5V4N54A---dQ-pFK.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-C4zmB1Vt.js → dagre-BM42HDAG-B_qe1ODY.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-n-BJmsiC.js → diagram-2AECGRRQ-Bou8vEYl.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-BzQGQnZi.js → diagram-5GNKFQAL-DEUEzONU.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-Cx_De-wS.js → diagram-KO2AKTUF-Ctq571VA.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-CIOI3Lok.js → diagram-LMA3HP47-CnKn89lm.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-DkRuPLWY.js → diagram-OG6HWLK6-DnKlrMx2.js} +1 -1
- package/workframe-ui/public/assets/{dist-XhCT1Q-V.js → dist-ubrEgQyZ.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-C3GwhDU1.js → erDiagram-TEJ5UH35-PjqO7CJd.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-g-Xo_eS4.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-CLWH8z2e.js → flowDiagram-I6XJVG4X-B2PN_nNG.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-D4h66Vy3.js → ganttDiagram-6RSMTGT7-ThyAtNQP.js} +1 -1
- package/workframe-ui/public/assets/gemini-Cq-0-rmr.svg +105 -0
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-IW5F-Q6c.js → gitGraph-WXDBUCRP-CB2UgoWy.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-BjNIbmRM.js → gitGraphDiagram-PVQCEYII-DRJJ-zT-.js} +1 -1
- package/workframe-ui/public/assets/index-1euVfd3b.js +132 -0
- package/workframe-ui/public/assets/index-BLVAsAeO.css +1 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-CcJspM79.js → info-J43DQDTF-CLtR33Q_.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-DP4xR9jt.js → infoDiagram-5YYISTIA-Cmunqfvi.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-D4cZb21V.js → ishikawaDiagram-YF4QCWOH-DWE_84eL.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-DsLuHkIJ.js → journeyDiagram-JHISSGLW-BtBhpyRL.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-Wu65nOsR.js → kanban-definition-UN3LZRKU-B8QspDDL.js} +1 -1
- package/workframe-ui/public/assets/{line-ByBDzRpy.js → line-iOgDn4O8.js} +1 -1
- package/workframe-ui/public/assets/{linear-DzXi2vnq.js → linear-BBehZNVA.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-CaB-1Ckn.js → mermaid-parser.core-DQK5HcdU.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-D7HTHC17.js → mermaid.core-Dsrdd1Wr.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-BfKsC9aj.js → mindmap-definition-RKZ34NQL-Ds_BfsJA.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-ClpU98TO.js → packet-YPE3B663-Bz3pi4i8.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-KeOyY_-u.js → pie-LRSECV5Y-BFFah-6g.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-CUpjnzbs.js → pieDiagram-4H26LBE5-CVCvLPcC.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-CJdPTszF.js → quadrantDiagram-W4KKPZXB-Cx6rrc_7.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-C9BpandZ.js → radar-GUYGQ44K-DJ5vN_cg.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-CHDZPwry.js → requirementDiagram-4Y6WPE33-Cbry6b5c.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-CMJJh91s.js → sankeyDiagram-5OEKKPKP-CICUr5En.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-BsV4GppP.js → sequenceDiagram-3UESZ5HK-BsOuB3F-.js} +1 -1
- package/workframe-ui/public/assets/{src-WHks82Up.js → src-TS8jZspq.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-eSX9P8z0.js → stateDiagram-AJRCARHV-avBzMQy_.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-BFRA5TYD.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-Dg1UkhGo.js → timeline-definition-PNZ67QCA-DHLm6UFN.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-B7yGsBuj.js → treeView-BLDUP644-B3Qd5t3j.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-j_G2oyQ_.js → treemap-LRROVOQU-D1dMQS7x.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-BRrT0jqC.js → vennDiagram-CIIHVFJN-DZ6SoniY.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-DEdUqez2.js → wardley-L42UT6IY-9gK17Tp3.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-C-bBK9Bb.js → wardleyDiagram-YWT4CUSO-FCU_wmRE.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DhH5xuR2.js → xychartDiagram-2RQKCTM6-DWowvseN.js} +1 -1
- package/workframe-ui/public/index.html +2 -2
- package/workframe-ui/public/workframe-build.json +3 -3
- package/docs/security.md +0 -85
- package/workframe-ui/public/assets/architecture-7EHR7CIX-m1aPUQ_r.js +0 -1
- package/workframe-ui/public/assets/channel-3M69ekE5.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-BMl9ed8P.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-C08HH6BU.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BFcOE4Aq.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BFcOE4Aq.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-DIVBL6CF.js +0 -1
- package/workframe-ui/public/assets/index-DL2vJP4L.css +0 -1
- package/workframe-ui/public/assets/index-ElS_D59P.js +0 -130
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-FzMK4PkM.js +0 -1
|
@@ -32,7 +32,7 @@ PLAYBOOK: dict[str, dict[str, Any]] = {
|
|
|
32
32
|
},
|
|
33
33
|
"provider_invalid_key": {
|
|
34
34
|
"message": "Your API key was rejected by the provider.",
|
|
35
|
-
"hint": "
|
|
35
|
+
"hint": "The key may be expired or revoked. Paste a new one under Settings → Integrations.",
|
|
36
36
|
"action": _ACTIONS["open_providers"],
|
|
37
37
|
"action_label": "Update API key",
|
|
38
38
|
"secondary_action": _ACTIONS["openrouter_keys"],
|
|
@@ -95,6 +95,38 @@ PLAYBOOK: dict[str, dict[str, Any]] = {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
_PROVIDER_LABELS: dict[str, str] = {
|
|
99
|
+
"openrouter": "OpenRouter",
|
|
100
|
+
"openai": "OpenAI",
|
|
101
|
+
"anthropic": "Anthropic",
|
|
102
|
+
"google": "Google",
|
|
103
|
+
"deepseek": "DeepSeek",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _provider_label(provider: str) -> str:
|
|
108
|
+
key = str(provider or "openrouter").strip().lower()
|
|
109
|
+
return _PROVIDER_LABELS.get(key, key.title() or "Provider")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def tailor_provider_entry(entry: dict[str, Any], provider: str) -> dict[str, Any]:
|
|
113
|
+
"""Agent-facing copy when a provider rejects credentials."""
|
|
114
|
+
if str(entry.get("code") or "") != "provider_invalid_key":
|
|
115
|
+
return entry
|
|
116
|
+
label = _provider_label(provider)
|
|
117
|
+
out = dict(entry)
|
|
118
|
+
out["message"] = (
|
|
119
|
+
f"I couldn't authenticate with {label} — your API key may be expired or revoked."
|
|
120
|
+
)
|
|
121
|
+
out["hint"] = (
|
|
122
|
+
f"Open Settings → Integrations to paste a new {label} key, then send your message again."
|
|
123
|
+
)
|
|
124
|
+
if str(provider or "").strip().lower() == "openrouter":
|
|
125
|
+
out["secondary_action"] = _ACTIONS["openrouter_keys"]
|
|
126
|
+
out["secondary_action_label"] = "Manage OpenRouter keys"
|
|
127
|
+
return out
|
|
128
|
+
|
|
129
|
+
|
|
98
130
|
def playbook_entry(code: str, **overrides: Any) -> dict[str, Any]:
|
|
99
131
|
"""Return a copy of a playbook row with optional string format overrides."""
|
|
100
132
|
base = dict(PLAYBOOK.get(code) or PLAYBOOK["unknown_provider_error"])
|
|
@@ -156,8 +188,17 @@ def classify_upstream(
|
|
|
156
188
|
low = msg.lower()
|
|
157
189
|
model_id = model or _model_from_text(msg)
|
|
158
190
|
|
|
159
|
-
if
|
|
160
|
-
|
|
191
|
+
if (
|
|
192
|
+
status == 401
|
|
193
|
+
or "invalid api key" in low
|
|
194
|
+
or "incorrect api key" in low
|
|
195
|
+
or "unauthorized" in low
|
|
196
|
+
or "user not found" in low
|
|
197
|
+
):
|
|
198
|
+
return tailor_provider_entry(
|
|
199
|
+
playbook_entry("provider_invalid_key", provider=provider),
|
|
200
|
+
provider,
|
|
201
|
+
)
|
|
161
202
|
if status == 402 or "insufficient" in low and "credit" in low or "payment required" in low:
|
|
162
203
|
return playbook_entry("provider_no_credits", provider=provider)
|
|
163
204
|
if status == 429 or "rate limit" in low:
|
|
@@ -197,11 +238,18 @@ def classify_exception_text(text: str) -> dict[str, Any]:
|
|
|
197
238
|
return classify_upstream(429, raw)
|
|
198
239
|
if "credit" in low:
|
|
199
240
|
return classify_upstream(402, raw)
|
|
200
|
-
if "api key" in low or "unauthorized" in low:
|
|
241
|
+
if "api key" in low or "unauthorized" in low or "user not found" in low:
|
|
201
242
|
return classify_upstream(401, raw)
|
|
202
243
|
return playbook_entry("unknown_provider_error", detail=raw[:200])
|
|
203
244
|
|
|
204
245
|
|
|
246
|
+
def classify_stream_error(text: str, *, provider: str = "openrouter") -> dict[str, Any]:
|
|
247
|
+
"""Classify upstream/Hermes failure text into UI notice payload."""
|
|
248
|
+
entry = classify_exception_text(str(text or "").strip())
|
|
249
|
+
entry = tailor_provider_entry(entry, provider)
|
|
250
|
+
return notice_payload(entry)
|
|
251
|
+
|
|
252
|
+
|
|
205
253
|
def notice_payload(entry: dict[str, Any]) -> dict[str, Any]:
|
|
206
254
|
"""SSE/JSON-safe dict for UI."""
|
|
207
255
|
out: dict[str, Any] = {
|
|
@@ -233,4 +281,10 @@ if __name__ == "__main__":
|
|
|
233
281
|
assert e2["code"] == "no_llm_provider"
|
|
234
282
|
e3 = classify_upstream(401, b'{"error":"Invalid API key"}')
|
|
235
283
|
assert e3["code"] == "provider_invalid_key"
|
|
284
|
+
e4 = classify_exception_text("HTTP 401: User not found.")
|
|
285
|
+
assert e4["code"] == "provider_invalid_key"
|
|
286
|
+
assert "OpenRouter" in e4["message"] or "authenticate" in e4["message"]
|
|
287
|
+
p = classify_stream_error("HTTP 401: User not found.", provider="openrouter")
|
|
288
|
+
assert p["code"] == "provider_invalid_key"
|
|
289
|
+
assert p.get("action", {}).get("type") == "open_settings"
|
|
236
290
|
print("llm_error_glossary ok")
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
import sqlite3
|
|
7
7
|
import time
|
|
8
|
+
import urllib.error
|
|
8
9
|
import urllib.parse
|
|
9
10
|
import urllib.request
|
|
10
11
|
import uuid
|
|
@@ -70,14 +71,22 @@ def _invoke_room_agent_mention(
|
|
|
70
71
|
template_slug = str(agent_row.get("slug") or "").strip()
|
|
71
72
|
agent_db_id = str(agent_row.get("id") or "").strip()
|
|
72
73
|
agent_name = str(agent_row.get("display_name") or template_slug).strip() or template_slug
|
|
73
|
-
if triggered_by_user_id:
|
|
74
|
-
personal = _srv()._runtime_display_label(triggered_by_user_id, template_slug, workspace_id)
|
|
75
|
-
if personal:
|
|
76
|
-
agent_name = personal
|
|
77
74
|
if not template_slug or not agent_db_id:
|
|
78
75
|
return
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
# Shared rooms use the native/template Hermes profile and one shared
|
|
77
|
+
# session. The triggering user's vault credential is overlaid per turn
|
|
78
|
+
# below; do not switch the room onto that user's private runtime profile.
|
|
79
|
+
hermes_slug = template_slug
|
|
80
|
+
display_model = str(agent_row.get("model_name") or "").strip()
|
|
81
|
+
display_provider = str(agent_row.get("model_provider") or "").strip()
|
|
82
|
+
if not display_model:
|
|
83
|
+
display_model = str(_srv()._read_model_block(hermes_slug).get("default") or "").strip()
|
|
84
|
+
if not display_provider:
|
|
85
|
+
display_provider = _srv()._llm_billing_provider(
|
|
86
|
+
hermes_slug,
|
|
87
|
+
user_id=triggered_by_user_id,
|
|
88
|
+
workspace_id=workspace_id,
|
|
89
|
+
)
|
|
81
90
|
|
|
82
91
|
turn_id = str(uuid.uuid4())
|
|
83
92
|
segments: list[dict[str, Any]] = []
|
|
@@ -200,6 +209,8 @@ def _invoke_room_agent_mention(
|
|
|
200
209
|
"agent_slug": hermes_slug,
|
|
201
210
|
"agent_name": agent_name,
|
|
202
211
|
"agent_profile_id": agent_db_id,
|
|
212
|
+
"model": display_model,
|
|
213
|
+
"llm_provider": display_provider,
|
|
203
214
|
"triggered_by_user_id": triggered_by_user_id,
|
|
204
215
|
},
|
|
205
216
|
)
|
|
@@ -276,7 +287,7 @@ def _invoke_room_agent_mention(
|
|
|
276
287
|
if not auth_decision.allowed:
|
|
277
288
|
fail_turn(
|
|
278
289
|
"I can't reply yet — connect an LLM provider in Profile → Connect accounts.",
|
|
279
|
-
persist=
|
|
290
|
+
persist=True,
|
|
280
291
|
)
|
|
281
292
|
return
|
|
282
293
|
|
|
@@ -299,28 +310,79 @@ def _invoke_room_agent_mention(
|
|
|
299
310
|
"Content-Type": "application/json",
|
|
300
311
|
}
|
|
301
312
|
req = urllib.request.Request(url, data=upstream_body, headers=headers, method="POST")
|
|
302
|
-
upstream = urllib.request.urlopen(req, timeout=3600)
|
|
303
|
-
|
|
304
313
|
final_text = ""
|
|
314
|
+
upstream = None
|
|
305
315
|
try:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
316
|
+
upstream = urllib.request.urlopen(req, timeout=3600)
|
|
317
|
+
except urllib.error.HTTPError as exc:
|
|
318
|
+
# Older Hermes runtimes expose the session chat endpoint but not
|
|
319
|
+
# the streaming variant. Room mentions should still work there;
|
|
320
|
+
# use the same non-stream request as the DM path.
|
|
321
|
+
if exc.code != 404:
|
|
322
|
+
raise
|
|
323
|
+
status, data = _srv()._profile_api_request(
|
|
324
|
+
hermes_slug,
|
|
325
|
+
"POST",
|
|
326
|
+
f"/api/sessions/{urllib.parse.quote(session_id, safe='')}/chat",
|
|
327
|
+
turn_body,
|
|
328
|
+
)
|
|
329
|
+
error_obj = data.get("error") if isinstance(data, dict) else None
|
|
330
|
+
error_code = error_obj.get("code") if isinstance(error_obj, dict) else ""
|
|
331
|
+
if status == 404 and error_code == "session_not_found":
|
|
332
|
+
# Hermes session state can be reset independently of the
|
|
333
|
+
# Workframe DB. Rebind this room once, then retry the turn.
|
|
334
|
+
session = lane_bindings.profile_chat_session(
|
|
335
|
+
hermes_slug,
|
|
336
|
+
{
|
|
337
|
+
"room_id": room_id,
|
|
338
|
+
"source_id": "room",
|
|
339
|
+
"client_id": room_id,
|
|
340
|
+
"workspace_id": workspace_id,
|
|
341
|
+
"new_session": True,
|
|
342
|
+
},
|
|
343
|
+
triggered_by_user_id,
|
|
344
|
+
)
|
|
345
|
+
session_id = str(session.get("session_id") or "").strip()
|
|
346
|
+
if not session_id:
|
|
347
|
+
raise ValueError("session_bootstrap_failed: could not recreate agent session for this room")
|
|
348
|
+
turn_body = profile_gateway._profile_turn_payload(hermes_slug, user_text, room_id)
|
|
349
|
+
_inject_turn_credentials(turn_body, triggered_by_user_id, workspace_id, provider)
|
|
350
|
+
status, data = _srv()._profile_api_request(
|
|
351
|
+
hermes_slug,
|
|
352
|
+
"POST",
|
|
353
|
+
f"/api/sessions/{urllib.parse.quote(session_id, safe='')}/chat",
|
|
354
|
+
turn_body,
|
|
355
|
+
)
|
|
356
|
+
if status >= 300:
|
|
357
|
+
raise ValueError(f"session chat failed: {data}")
|
|
358
|
+
if isinstance(data, dict):
|
|
359
|
+
message = data.get("message")
|
|
360
|
+
if isinstance(message, dict):
|
|
361
|
+
final_text = str(message.get("content") or message.get("text") or "")
|
|
362
|
+
if not final_text:
|
|
363
|
+
final_text = str(data.get("content") or data.get("text") or "")
|
|
364
|
+
publish_update(force=True, status="writing")
|
|
365
|
+
|
|
366
|
+
if upstream is not None:
|
|
367
|
+
try:
|
|
368
|
+
publish_update(force=True, status="thinking")
|
|
369
|
+
for event_name, data in chat_stream._iter_profile_stream_frames(upstream):
|
|
370
|
+
segments[:] = chat_stream._live_reduce_stream_event(segments, event_name, data)
|
|
371
|
+
if event_name in ("message.delta", "assistant.delta"):
|
|
372
|
+
final_text += chat_stream._live_stream_text(data)
|
|
373
|
+
publish_update(status="writing")
|
|
374
|
+
elif event_name in ("thinking.delta", "reasoning.delta"):
|
|
375
|
+
publish_update(status="thinking")
|
|
376
|
+
elif event_name == "tool.started":
|
|
377
|
+
publish_update(force=True, status=f"tool:{data.get('tool_name') or 'tool'}")
|
|
378
|
+
elif event_name in ("tool.completed", "tool.failed", "tool.progress"):
|
|
379
|
+
publish_update(force=True)
|
|
380
|
+
elif event_name in ("message.complete", "assistant.completed"):
|
|
381
|
+
final_text = str(data.get("content") or data.get("text") or final_text)
|
|
382
|
+
publish_update(force=True, status="writing")
|
|
383
|
+
publish_update(force=True)
|
|
384
|
+
finally:
|
|
385
|
+
upstream.close()
|
|
324
386
|
|
|
325
387
|
assistant = final_text.strip() or chat_stream._segments_to_reply_text(segments)
|
|
326
388
|
if not assistant:
|
|
@@ -493,7 +493,7 @@ def _apply_model_for_billing_provider(
|
|
|
493
493
|
ok_provider, _ = _set_profile_model_provider(profile, "custom")
|
|
494
494
|
if not ok_provider:
|
|
495
495
|
return False
|
|
496
|
-
_set_profile_model_base_url(profile, _srv()._llm_proxy_base_url(billing))
|
|
496
|
+
_srv()._set_profile_model_base_url(profile, _srv()._llm_proxy_base_url(billing))
|
|
497
497
|
return True
|
|
498
498
|
|
|
499
499
|
|
|
@@ -434,6 +434,33 @@ def _parse_device_oauth_log(text: str) -> dict[str, str | None]:
|
|
|
434
434
|
return {"verification_uri": verification_uri, "user_code": user_code}
|
|
435
435
|
|
|
436
436
|
|
|
437
|
+
def _device_oauth_error_from_log(log_text: str) -> str | None:
|
|
438
|
+
clean = _strip_ansi(log_text).strip()
|
|
439
|
+
if not clean:
|
|
440
|
+
return None
|
|
441
|
+
lowered = clean.lower()
|
|
442
|
+
auth_err = re.search(r"AuthError:\s*(.+)", clean)
|
|
443
|
+
if auth_err:
|
|
444
|
+
return auth_err.group(1).strip()[:500]
|
|
445
|
+
if "rate-limit" in lowered or "rate limiting" in lowered or "http 429" in lowered or re.search(r"\b429\b", lowered):
|
|
446
|
+
return (
|
|
447
|
+
"OpenAI is rate-limiting Codex login requests (HTTP 429). "
|
|
448
|
+
"Wait a minute and try again."
|
|
449
|
+
)
|
|
450
|
+
if any(token in lowered for token in ("login timed out", "login cancelled", "oauth_start_failed")):
|
|
451
|
+
lines = [line.strip() for line in clean.splitlines() if line.strip()]
|
|
452
|
+
return (lines[-1] if lines else "OAuth failed")[:500]
|
|
453
|
+
if "traceback" in lowered and any(token in lowered for token in ("error", "exception", "failed")):
|
|
454
|
+
for line in reversed(clean.splitlines()):
|
|
455
|
+
stripped = line.strip()
|
|
456
|
+
if not stripped or stripped.startswith("File ") or "Traceback" in stripped:
|
|
457
|
+
continue
|
|
458
|
+
if stripped.endswith(":") and "Error" not in stripped:
|
|
459
|
+
continue
|
|
460
|
+
return stripped[:500]
|
|
461
|
+
return None
|
|
462
|
+
|
|
463
|
+
|
|
437
464
|
def _sync_user_oauth_provider_to_runtime_profiles(user_id: str, hermes_auth_id: str) -> None:
|
|
438
465
|
if not _hermes_oauth_tokens_present(user_id, hermes_auth_id):
|
|
439
466
|
return
|
|
@@ -527,11 +554,13 @@ def _start_device_oauth(user_id: str, provider_id: str, spec: dict[str, Any]) ->
|
|
|
527
554
|
except (RuntimeError, ValueError) as exc:
|
|
528
555
|
return {"ok": False, "provider": provider_id, "error": str(exc)}
|
|
529
556
|
if rc != 0:
|
|
557
|
+
output = (out or "").strip()
|
|
530
558
|
return {
|
|
531
559
|
"ok": False,
|
|
532
560
|
"provider": provider_id,
|
|
533
561
|
"error": "oauth_start_failed",
|
|
534
|
-
"output":
|
|
562
|
+
"output": output,
|
|
563
|
+
"message": _device_oauth_error_from_log(output) or output[-500:] or "Could not start OAuth",
|
|
535
564
|
}
|
|
536
565
|
with _oauth_device_lock:
|
|
537
566
|
_oauth_device_sessions[session_id] = {
|
|
@@ -583,6 +612,19 @@ def device_oauth_status(user_id: str, provider_id: str, session_id: str) -> dict
|
|
|
583
612
|
if patch:
|
|
584
613
|
_device_oauth_session_patch(session_id, patch)
|
|
585
614
|
sess.update(patch)
|
|
615
|
+
log_error = _device_oauth_error_from_log(log_text)
|
|
616
|
+
if log_error:
|
|
617
|
+
_device_oauth_session_patch(session_id, {"status": "error"})
|
|
618
|
+
return {
|
|
619
|
+
"ok": False,
|
|
620
|
+
"provider": provider_id,
|
|
621
|
+
"session_id": session_id,
|
|
622
|
+
"status": "error",
|
|
623
|
+
"error": "oauth_failed",
|
|
624
|
+
"message": log_error,
|
|
625
|
+
"verification_uri": sess.get("verification_uri"),
|
|
626
|
+
"user_code": sess.get("user_code"),
|
|
627
|
+
}
|
|
586
628
|
if _hermes_oauth_tokens_present(user_id, hermes_auth_id):
|
|
587
629
|
if not sess.get("finalized"):
|
|
588
630
|
_finalize_hermes_device_oauth(user_id, provider_id, spec)
|
|
@@ -617,7 +659,7 @@ def device_oauth_status(user_id: str, provider_id: str, session_id: str) -> dict
|
|
|
617
659
|
"session_id": session_id,
|
|
618
660
|
"status": "error",
|
|
619
661
|
"error": "oauth_failed",
|
|
620
|
-
"message": _strip_ansi(log_text).strip()[-500:] or "OAuth failed",
|
|
662
|
+
"message": _device_oauth_error_from_log(log_text) or _strip_ansi(log_text).strip()[-500:] or "OAuth failed",
|
|
621
663
|
"verification_uri": sess.get("verification_uri"),
|
|
622
664
|
"user_code": sess.get("user_code"),
|
|
623
665
|
}
|
|
@@ -130,6 +130,15 @@ def _user_llm_providers_for_picker(user_id: str) -> set[str]:
|
|
|
130
130
|
return set(connected)
|
|
131
131
|
|
|
132
132
|
|
|
133
|
+
def invalidate_user_llm_picker_cache(user_id: str = "") -> None:
|
|
134
|
+
"""Forget cached provider availability after a credential mutation."""
|
|
135
|
+
user = str(user_id or "").strip()
|
|
136
|
+
if user:
|
|
137
|
+
_srv()._user_llm_picker_cache.pop(user, None)
|
|
138
|
+
else:
|
|
139
|
+
_srv()._user_llm_picker_cache.clear()
|
|
140
|
+
|
|
141
|
+
|
|
133
142
|
def _user_has_llm_provider(user_id: str) -> bool:
|
|
134
143
|
return bool(_srv()._user_llm_providers_for_picker(user_id))
|
|
135
144
|
|
|
@@ -12,7 +12,7 @@ PROVIDER_CONNECT_CATALOG: tuple[dict[str, Any], ...] = (
|
|
|
12
12
|
"category": "llm",
|
|
13
13
|
"connect_mode": "api_key",
|
|
14
14
|
"env_var": "OPENROUTER_API_KEY",
|
|
15
|
-
"description": "Primary LLM router
|
|
15
|
+
"description": "Primary LLM router — any model via one API key.",
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"id": "anthropic",
|
|
@@ -104,7 +104,7 @@ PROVIDER_CONNECT_CATALOG: tuple[dict[str, Any], ...] = (
|
|
|
104
104
|
"oauth_provider": "github",
|
|
105
105
|
"hermes_auth_id": "github",
|
|
106
106
|
"user_only": True,
|
|
107
|
-
"description": "OAuth (admin registers app) or fine-grained PAT
|
|
107
|
+
"description": "OAuth (admin registers app) or fine-grained PAT — repo push and GitHub API.",
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
110
|
"id": "stripe",
|
|
@@ -114,7 +114,7 @@ PROVIDER_CONNECT_CATALOG: tuple[dict[str, Any], ...] = (
|
|
|
114
114
|
"env_var": "STRIPE_SECRET_KEY",
|
|
115
115
|
"oauth_provider": "stripe",
|
|
116
116
|
"user_only": True,
|
|
117
|
-
"description": "Connect your Stripe account
|
|
117
|
+
"description": "Connect your Stripe account — charges, customers, and billing tools.",
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
"id": "vercel",
|
package/workframe-api/rooms.py
CHANGED
|
@@ -1304,6 +1304,9 @@ def _workspace_credential_mode(conn: sqlite3.Connection | None, workspace_id: st
|
|
|
1304
1304
|
settings = _srv()._parse_workspace_settings(row) if row else {}
|
|
1305
1305
|
mode = str(settings.get("credential_mode") or "byok").strip().lower()
|
|
1306
1306
|
return mode if mode in ("byok", "workspace") else "byok"
|
|
1307
|
+
except sqlite3.Error:
|
|
1308
|
+
# Before bootstrap creates the workspace schema, BYOK is the safe default.
|
|
1309
|
+
return "byok"
|
|
1307
1310
|
finally:
|
|
1308
1311
|
if own_conn:
|
|
1309
1312
|
conn.close()
|
|
@@ -146,6 +146,7 @@ ROUTES: tuple[Route, ...] = (
|
|
|
146
146
|
Route("POST", "/api/admin/vault/wipe", AuthLevel.ROLE_OWNER_ADMIN, "_route_post_admin_vault_wipe"),
|
|
147
147
|
Route("POST", "/api/install/stack/branding-asset", AuthLevel.ROLE_OWNER_ADMIN, "_route_post_install_stack_branding_asset"),
|
|
148
148
|
# --- install-window POST ---
|
|
149
|
+
Route("POST", "/api/install/register-admin", AuthLevel.PUBLIC, "_route_post_install_register_admin", install_window=True),
|
|
149
150
|
Route("POST", "/api/install/email/test", AuthLevel.PUBLIC, "_route_post_install_email_test", install_window=True),
|
|
150
151
|
Route("POST", "/api/install/url/test", AuthLevel.PUBLIC, "_route_post_install_url_test", install_window=True),
|
|
151
152
|
Route("POST", "/api/install/setup-https", AuthLevel.PUBLIC, "_route_post_install_setup_https", install_window=True),
|
|
@@ -11,6 +11,7 @@ from datetime import datetime, timezone
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any, Mapping
|
|
13
13
|
|
|
14
|
+
import llm_error_glossary
|
|
14
15
|
from domain.entities import (
|
|
15
16
|
ActorType,
|
|
16
17
|
FundingSource,
|
|
@@ -423,18 +424,21 @@ def list_run_events_for_room(room_id: str, *, limit: int = 40) -> list[dict[str,
|
|
|
423
424
|
payload = json.loads(str(row["payload_json"] or "{}"))
|
|
424
425
|
except json.JSONDecodeError:
|
|
425
426
|
payload = {}
|
|
427
|
+
event_type = str(row["event_type"])
|
|
428
|
+
if event_type == "run.authorized":
|
|
429
|
+
continue
|
|
426
430
|
out.append(
|
|
427
431
|
{
|
|
428
432
|
"id": f"run_event:{row['event_id']}",
|
|
429
433
|
"kind": "run_event",
|
|
430
|
-
"event_type":
|
|
434
|
+
"event_type": event_type,
|
|
431
435
|
"run_id": str(row["run_id"]),
|
|
432
436
|
"run_status": str(row["run_status"]),
|
|
433
437
|
"profile": str(row["profile_slug"] or ""),
|
|
434
438
|
"payer_user_id": str(row["payer_user_id"] or ""),
|
|
435
439
|
"funding_source": str(row["funding_source"] or ""),
|
|
436
|
-
"task_description": _event_description(
|
|
437
|
-
"status": str(row["run_status"]),
|
|
440
|
+
"task_description": _event_description(event_type, payload),
|
|
441
|
+
"status": _activity_status_for_event(event_type, str(row["run_status"])),
|
|
438
442
|
"created_at": str(row["created_at"]),
|
|
439
443
|
"source": "run_ledger",
|
|
440
444
|
"payload": payload,
|
|
@@ -443,16 +447,56 @@ def list_run_events_for_room(room_id: str, *, limit: int = 40) -> list[dict[str,
|
|
|
443
447
|
return out
|
|
444
448
|
|
|
445
449
|
|
|
450
|
+
def _activity_status_for_event(event_type: str, run_status: str) -> str:
|
|
451
|
+
et = str(event_type or "").strip().lower()
|
|
452
|
+
if et == "run.completed":
|
|
453
|
+
return "completed"
|
|
454
|
+
if et in ("run.failed", "run.denied"):
|
|
455
|
+
return "failed"
|
|
456
|
+
rs = str(run_status or "").strip().lower()
|
|
457
|
+
if rs == "running":
|
|
458
|
+
return "active"
|
|
459
|
+
if rs == "completed":
|
|
460
|
+
return "completed"
|
|
461
|
+
if rs in ("failed", "denied", "cancelled"):
|
|
462
|
+
return "failed"
|
|
463
|
+
return "idle"
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _humanize_run_failure(reason: str) -> str:
|
|
467
|
+
raw = str(reason or "").strip()
|
|
468
|
+
if not raw:
|
|
469
|
+
return "Turn failed"
|
|
470
|
+
entry = llm_error_glossary.classify_exception_text(raw)
|
|
471
|
+
msg = str(entry.get("message") or "").strip()
|
|
472
|
+
if msg:
|
|
473
|
+
return msg
|
|
474
|
+
return raw[:160]
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _humanize_deny_reason(reason: str) -> str:
|
|
478
|
+
key = str(reason or "").strip().lower()
|
|
479
|
+
labels = {
|
|
480
|
+
"no_credential_byok": "connect an LLM provider in Settings",
|
|
481
|
+
"no_credential": "connect an LLM provider in Settings",
|
|
482
|
+
"policy": "policy blocked this turn",
|
|
483
|
+
}
|
|
484
|
+
return labels.get(key, key.replace("_", " ") or "policy")
|
|
485
|
+
|
|
486
|
+
|
|
446
487
|
def _event_description(event_type: str, payload: Mapping[str, Any]) -> str:
|
|
447
488
|
if event_type == "run.authorized":
|
|
448
|
-
|
|
449
|
-
return f"Run authorized ({fs})" if fs else "Run authorized"
|
|
489
|
+
return ""
|
|
450
490
|
if event_type == "run.denied":
|
|
451
|
-
return f"
|
|
491
|
+
return f"Turn blocked — {_humanize_deny_reason(str(payload.get('deny_reason') or 'policy'))}"
|
|
452
492
|
if event_type == "run.completed":
|
|
453
|
-
|
|
493
|
+
model = str(payload.get("model") or "").strip()
|
|
494
|
+
if model:
|
|
495
|
+
short = model.rsplit("/", 1)[-1]
|
|
496
|
+
return f"Reply delivered · {short}"
|
|
497
|
+
return "Reply delivered"
|
|
454
498
|
if event_type == "run.failed":
|
|
455
|
-
return
|
|
499
|
+
return _humanize_run_failure(str(payload.get("reason") or payload.get("code") or ""))
|
|
456
500
|
if event_type == "slash.command":
|
|
457
501
|
cmd = str(payload.get("command") or "")
|
|
458
502
|
return f"Slash /{cmd}" if cmd else "Slash command"
|
|
@@ -570,10 +570,12 @@ def _purge_runtime_profile(runtime: str) -> None:
|
|
|
570
570
|
|
|
571
571
|
|
|
572
572
|
def _register_runtime_profile(runtime: str, template: str) -> None:
|
|
573
|
+
_srv()._ensure_profiles_dir_ready()
|
|
573
574
|
cmd = ["profile", "create", "--clone-from", template, runtime]
|
|
574
575
|
code, out = _srv()._gateway_exec(_srv()._primary_profile(), cmd)
|
|
575
576
|
if code == 0:
|
|
576
577
|
_inherit_runtime_profile_config(runtime, template)
|
|
578
|
+
_srv()._chown_profile_tree(_srv()._profile_dir(runtime))
|
|
577
579
|
return
|
|
578
580
|
out_l = out.lower()
|
|
579
581
|
if "already exists" in out_l:
|
|
@@ -583,6 +585,7 @@ def _register_runtime_profile(runtime: str, template: str) -> None:
|
|
|
583
585
|
code, out = _srv()._gateway_exec(_srv()._primary_profile(), cmd)
|
|
584
586
|
if code == 0:
|
|
585
587
|
_inherit_runtime_profile_config(runtime, template)
|
|
588
|
+
_srv()._chown_profile_tree(_srv()._profile_dir(runtime))
|
|
586
589
|
return
|
|
587
590
|
if code != 0:
|
|
588
591
|
raise ValueError(f"runtime profile create failed: {out.strip()}")
|
|
@@ -709,6 +712,7 @@ def ensure_runtime_profile(
|
|
|
709
712
|
user_id: str,
|
|
710
713
|
workspace_id: str = "",
|
|
711
714
|
) -> None:
|
|
715
|
+
_srv()._ensure_profiles_dir_ready()
|
|
712
716
|
runtime = _srv().safe_profile_slug(runtime_slug)
|
|
713
717
|
template = _srv().resolve_validated_profile(template_slug)
|
|
714
718
|
if _runtime_profile_on_disk(runtime):
|
|
@@ -726,10 +730,16 @@ def ensure_runtime_profile(
|
|
|
726
730
|
if not _profile_toolsets_ready(runtime, _srv()._chat_toolsets_for_profile(runtime)):
|
|
727
731
|
_srv()._ensure_profile_toolsets(runtime)
|
|
728
732
|
_ensure_profile_terminal_cwd(runtime)
|
|
729
|
-
#
|
|
733
|
+
# Existing profiles still need invariant repair. This deliberately does
|
|
734
|
+
# not copy template model preferences; reconciliation keeps the agent's
|
|
735
|
+
# model when the acting user can use it and falls back to that user's
|
|
736
|
+
# connected providers otherwise.
|
|
737
|
+
_prepare_runtime_profile_credentials(runtime, user_id, workspace_id)
|
|
738
|
+
_srv()._ensure_profile_proxy_headers(runtime)
|
|
730
739
|
return
|
|
731
740
|
_purge_runtime_profile(runtime)
|
|
732
741
|
_register_runtime_profile(runtime, template)
|
|
742
|
+
_srv()._chown_profile_tree(_srv()._profile_dir(runtime))
|
|
733
743
|
if _srv()._profile_config_path(runtime) is None:
|
|
734
744
|
raise ValueError(f"runtime profile bootstrap failed: {runtime}")
|
|
735
745
|
# SOUL: clone inherits template at profile create; do not auto-overwrite on disk later.
|
package/workframe-api/server.py
CHANGED
|
@@ -190,6 +190,7 @@ _register_runtime_profile = runtime_cohort._register_runtime_profile
|
|
|
190
190
|
_inherit_runtime_profile_config = runtime_cohort._inherit_runtime_profile_config
|
|
191
191
|
ensure_runtime_profile = runtime_cohort.ensure_runtime_profile
|
|
192
192
|
_ensure_profile_terminal_cwd = runtime_cohort._ensure_profile_terminal_cwd
|
|
193
|
+
_profile_toolsets_ready = runtime_cohort._profile_toolsets_ready
|
|
193
194
|
|
|
194
195
|
health_data = health_monitor.health_data
|
|
195
196
|
build_snapshot = snapshot_feed.build_snapshot
|
|
@@ -256,6 +257,7 @@ _user_has_llm_provider = provider_bootstrap._user_has_llm_provider
|
|
|
256
257
|
_user_llm_has_provider = provider_bootstrap._user_llm_has_provider
|
|
257
258
|
_workspace_has_llm_provider = provider_bootstrap._workspace_has_llm_provider
|
|
258
259
|
_user_can_use_llm = provider_bootstrap._user_can_use_llm
|
|
260
|
+
_invalidate_user_llm_picker_cache = provider_bootstrap.invalidate_user_llm_picker_cache
|
|
259
261
|
_overlay_chat_llm_env = provider_bootstrap._overlay_chat_llm_env
|
|
260
262
|
_reconcile_profile_llm_for_user = provider_bootstrap._reconcile_profile_llm_for_user
|
|
261
263
|
_ensure_profile_auth_pool = provider_bootstrap._ensure_profile_auth_pool
|
|
@@ -332,6 +334,7 @@ _native_profile_slug = hermes_profiles._native_profile_slug
|
|
|
332
334
|
_native_profile_present = hermes_profiles._native_profile_present
|
|
333
335
|
_hermes_data_uid_gid = hermes_profiles._hermes_data_uid_gid
|
|
334
336
|
_chown_profile_tree = hermes_profiles._chown_profile_tree
|
|
337
|
+
_ensure_profiles_dir_ready = hermes_profiles._ensure_profiles_dir_ready
|
|
335
338
|
_seed_native_profile_on_disk = hermes_profiles._seed_native_profile_on_disk
|
|
336
339
|
_ensure_native_hermes_profile = hermes_profiles._ensure_native_hermes_profile
|
|
337
340
|
_primary_profile = hermes_profiles._primary_profile
|
|
@@ -613,9 +616,14 @@ import zk_auth as _zk
|
|
|
613
616
|
_profile_health_cache: dict[str, tuple[bool, float]] = {}
|
|
614
617
|
_PROFILE_HEALTH_TTL_SEC = 8.0
|
|
615
618
|
_user_llm_picker_cache: dict[str, tuple[frozenset[str], float]] = {}
|
|
619
|
+
_USER_LLM_PICKER_TTL_SEC = 60.0
|
|
616
620
|
_SESSION_INFO_TTL_SEC = 5.0
|
|
617
621
|
|
|
618
622
|
|
|
623
|
+
def _invalidate_profile_health_cache(profile: str) -> None:
|
|
624
|
+
_profile_health_cache.pop(str(profile or "").strip(), None)
|
|
625
|
+
|
|
626
|
+
|
|
619
627
|
def _install_window_open() -> bool:
|
|
620
628
|
try:
|
|
621
629
|
return install_api.install_window_open(str(DATA_DIR / "workframe.db"))
|
|
@@ -1366,6 +1374,7 @@ _read_profile_lease_token = turn_overlay._read_profile_lease_token
|
|
|
1366
1374
|
_lease_reusable_for_turn = turn_overlay._lease_reusable_for_turn
|
|
1367
1375
|
_read_config_model_api_key = turn_overlay._read_config_model_api_key
|
|
1368
1376
|
_sync_profile_model_api_key = turn_overlay._sync_profile_model_api_key
|
|
1377
|
+
_schedule_profile_lease_yaml_reconcile = turn_overlay._schedule_profile_lease_yaml_reconcile
|
|
1369
1378
|
_apply_turn_credential_lease = turn_overlay._apply_turn_credential_lease
|
|
1370
1379
|
_action_proxy_base_url = turn_overlay._action_proxy_base_url
|
|
1371
1380
|
_action_proxy_env_var = turn_overlay._action_proxy_env_var
|
|
@@ -1388,6 +1397,7 @@ _parse_user_content_segments = chat_sessions._parse_user_content_segments
|
|
|
1388
1397
|
_parse_tool_content = chat_sessions._parse_tool_content
|
|
1389
1398
|
_message_row_segments = chat_sessions._message_row_segments
|
|
1390
1399
|
_llm_attribution_for_profile = chat_sessions._llm_attribution_for_profile
|
|
1400
|
+
_profile_model = chat_sessions._profile_model
|
|
1391
1401
|
_latest_session_id = chat_sessions._latest_session_id
|
|
1392
1402
|
_session_info = chat_sessions._session_info
|
|
1393
1403
|
_is_blank_session_title = chat_sessions._is_blank_session_title
|
|
@@ -1460,12 +1470,26 @@ def _enrich_room_messages(conn: sqlite3.Connection, messages: list[Any]) -> list
|
|
|
1460
1470
|
agent_id = str(item.get("sender_agent_id") or "").strip()
|
|
1461
1471
|
if agent_id:
|
|
1462
1472
|
agent = conn.execute(
|
|
1463
|
-
"
|
|
1473
|
+
"""
|
|
1474
|
+
SELECT slug, display_name, model_name, model_provider
|
|
1475
|
+
FROM agent_profiles WHERE id = ? AND deleted_at IS NULL
|
|
1476
|
+
""",
|
|
1464
1477
|
(agent_id,),
|
|
1465
1478
|
).fetchone()
|
|
1466
1479
|
if agent:
|
|
1467
1480
|
item["sender_agent_slug"] = str(agent["slug"])
|
|
1468
1481
|
item["sender_agent_name"] = str(agent["display_name"] or agent["slug"])
|
|
1482
|
+
model_name = str(agent["model_name"] or "").strip()
|
|
1483
|
+
model_provider = str(agent["model_provider"] or "").strip()
|
|
1484
|
+
slug = str(agent["slug"] or "").strip()
|
|
1485
|
+
if not model_name and slug:
|
|
1486
|
+
model_name = str(_read_model_block(slug).get("default") or "").strip()
|
|
1487
|
+
if not model_provider and slug:
|
|
1488
|
+
model_provider = _llm_billing_provider(slug)
|
|
1489
|
+
if model_name:
|
|
1490
|
+
item["model"] = model_name
|
|
1491
|
+
if model_provider:
|
|
1492
|
+
item["llm_provider"] = model_provider
|
|
1469
1493
|
enriched.append(item)
|
|
1470
1494
|
return enriched
|
|
1471
1495
|
|
|
@@ -375,7 +375,10 @@ def mark_install_admin_verified(email: str) -> None:
|
|
|
375
375
|
smtp["admin_email"] = normalized
|
|
376
376
|
smtp["admin_verified_at"] = int(time.time())
|
|
377
377
|
raw["smtp"] = smtp
|
|
378
|
-
raw
|
|
378
|
+
if not str(raw.get("deployment_mode") or "").strip():
|
|
379
|
+
raw["wizard_step"] = "welcome"
|
|
380
|
+
else:
|
|
381
|
+
raw["wizard_step"] = "workframe"
|
|
379
382
|
_write_raw(raw)
|
|
380
383
|
|
|
381
384
|
|