@researai/deepscientist 1.5.9 → 1.5.12
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 +112 -99
- package/assets/branding/connector-qq.png +0 -0
- package/assets/branding/connector-rokid.png +0 -0
- package/assets/branding/connector-weixin.png +0 -0
- package/assets/branding/projects.png +0 -0
- package/bin/ds.js +519 -63
- package/docs/assets/branding/projects.png +0 -0
- package/docs/en/00_QUICK_START.md +338 -68
- package/docs/en/01_SETTINGS_REFERENCE.md +14 -0
- package/docs/en/02_START_RESEARCH_GUIDE.md +180 -4
- package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +62 -179
- package/docs/en/09_DOCTOR.md +66 -5
- package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +137 -0
- package/docs/en/11_LICENSE_AND_RISK.md +256 -0
- package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +446 -0
- package/docs/en/13_CORE_ARCHITECTURE_GUIDE.md +297 -0
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
- package/docs/en/15_CODEX_PROVIDER_SETUP.md +284 -0
- package/docs/en/99_ACKNOWLEDGEMENTS.md +4 -1
- package/docs/en/README.md +83 -0
- package/docs/images/lingzhu/rokid-agent-platform-create.png +0 -0
- package/docs/images/weixin/weixin-plugin-entry.png +0 -0
- package/docs/images/weixin/weixin-plugin-entry.svg +33 -0
- package/docs/images/weixin/weixin-qr-confirm.svg +30 -0
- package/docs/images/weixin/weixin-quest-media-flow.svg +44 -0
- package/docs/images/weixin/weixin-settings-bind.svg +57 -0
- package/docs/zh/00_QUICK_START.md +345 -72
- package/docs/zh/01_SETTINGS_REFERENCE.md +14 -0
- package/docs/zh/02_START_RESEARCH_GUIDE.md +181 -3
- package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +62 -193
- package/docs/zh/09_DOCTOR.md +68 -5
- package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +144 -0
- package/docs/zh/11_LICENSE_AND_RISK.md +256 -0
- package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +442 -0
- package/docs/zh/13_CORE_ARCHITECTURE_GUIDE.md +296 -0
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
- package/docs/zh/15_CODEX_PROVIDER_SETUP.md +285 -0
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +4 -1
- package/docs/zh/README.md +129 -0
- package/install.sh +0 -34
- package/package.json +2 -2
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/annotations.py +343 -0
- package/src/deepscientist/artifact/arxiv.py +484 -37
- package/src/deepscientist/artifact/service.py +574 -108
- package/src/deepscientist/arxiv_library.py +275 -0
- package/src/deepscientist/bash_exec/monitor.py +7 -5
- package/src/deepscientist/bash_exec/service.py +93 -21
- package/src/deepscientist/bridges/builtins.py +2 -0
- package/src/deepscientist/bridges/connectors.py +447 -0
- package/src/deepscientist/channels/__init__.py +2 -0
- package/src/deepscientist/channels/builtins.py +3 -1
- package/src/deepscientist/channels/local.py +3 -3
- package/src/deepscientist/channels/qq.py +8 -8
- package/src/deepscientist/channels/qq_gateway.py +1 -1
- package/src/deepscientist/channels/relay.py +14 -8
- package/src/deepscientist/channels/weixin.py +59 -0
- package/src/deepscientist/channels/weixin_ilink.py +388 -0
- package/src/deepscientist/config/models.py +23 -2
- package/src/deepscientist/config/service.py +539 -67
- package/src/deepscientist/connector/__init__.py +4 -0
- package/src/deepscientist/connector/connector_profiles.py +481 -0
- package/src/deepscientist/connector/lingzhu_support.py +668 -0
- package/src/deepscientist/connector/qq_profiles.py +206 -0
- package/src/deepscientist/connector/weixin_support.py +663 -0
- package/src/deepscientist/connector_profiles.py +1 -374
- package/src/deepscientist/connector_runtime.py +2 -0
- package/src/deepscientist/daemon/api/handlers.py +165 -5
- package/src/deepscientist/daemon/api/router.py +13 -1
- package/src/deepscientist/daemon/app.py +1444 -67
- package/src/deepscientist/doctor.py +4 -5
- package/src/deepscientist/gitops/diff.py +120 -29
- package/src/deepscientist/lingzhu_support.py +1 -182
- package/src/deepscientist/mcp/server.py +135 -7
- package/src/deepscientist/prompts/builder.py +128 -11
- package/src/deepscientist/qq_profiles.py +1 -196
- package/src/deepscientist/quest/node_traces.py +23 -0
- package/src/deepscientist/quest/service.py +359 -74
- package/src/deepscientist/quest/stage_views.py +71 -5
- package/src/deepscientist/runners/codex.py +170 -19
- package/src/deepscientist/runners/runtime_overrides.py +6 -0
- package/src/deepscientist/shared.py +33 -14
- package/src/deepscientist/weixin_support.py +1 -0
- package/src/prompts/connectors/lingzhu.md +3 -1
- package/src/prompts/connectors/qq.md +2 -1
- package/src/prompts/connectors/weixin.md +231 -0
- package/src/prompts/contracts/shared_interaction.md +4 -1
- package/src/prompts/system.md +61 -9
- package/src/skills/analysis-campaign/SKILL.md +46 -6
- package/src/skills/analysis-campaign/references/campaign-plan-template.md +21 -8
- package/src/skills/baseline/SKILL.md +1 -1
- package/src/skills/decision/SKILL.md +1 -1
- package/src/skills/experiment/SKILL.md +1 -1
- package/src/skills/finalize/SKILL.md +1 -1
- package/src/skills/idea/SKILL.md +1 -1
- package/src/skills/intake-audit/SKILL.md +1 -1
- package/src/skills/rebuttal/SKILL.md +74 -1
- package/src/skills/rebuttal/references/response-letter-template.md +55 -11
- package/src/skills/review/SKILL.md +118 -1
- package/src/skills/review/references/experiment-todo-template.md +23 -0
- package/src/skills/review/references/review-report-template.md +16 -0
- package/src/skills/review/references/revision-log-template.md +4 -0
- package/src/skills/scout/SKILL.md +1 -1
- package/src/skills/write/SKILL.md +168 -7
- package/src/skills/write/references/paper-experiment-matrix-template.md +131 -0
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-BKZ103sn.js → AiManusChatView-CnJcXynW.js} +156 -48
- package/src/ui/dist/assets/{AnalysisPlugin-mTTzGAlK.js → AnalysisPlugin-DeyzPEhV.js} +1 -1
- package/src/ui/dist/assets/{CliPlugin-BH58n3GY.js → CliPlugin-CB1YODQn.js} +164 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-BKGRUH7e.js → CodeEditorPlugin-B-xicq1e.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-BMADwFWJ.js → CodeViewerPlugin-DT54ysXa.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-ZOnTIHLN.js → DocViewerPlugin-DQtKT-VD.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-CQ7h1Djm.js → GitDiffViewerPlugin-hqHbCfnv.js} +20 -21
- package/src/ui/dist/assets/{ImageViewerPlugin-GVS5MsnC.js → ImageViewerPlugin-OcVo33jV.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-BZNv1JML.js → LabCopilotPanel-DdGwhEUV.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-TWcJsdQA.js → LabPlugin-Ciz1gDaX.js} +2 -1
- package/src/ui/dist/assets/{LatexPlugin-DIjHiR2x.js → LatexPlugin-BhmjNQRC.js} +37 -11
- package/src/ui/dist/assets/{MarkdownViewerPlugin-D3ooGAH0.js → MarkdownViewerPlugin-BzdVH9Bx.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DfVfE9hN.js → MarketplacePlugin-DmyHspXt.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-DDl0_Mc0.js → NotebookEditor-BMXKrDRk.js} +1 -1
- package/src/ui/dist/assets/{NotebookEditor-s8JhzuX1.js → NotebookEditor-BTVYRGkm.js} +12 -12
- package/src/ui/dist/assets/{PdfLoader-C2Sf6SJM.js → PdfLoader-CvcjJHXv.js} +14 -7
- package/src/ui/dist/assets/{PdfMarkdownPlugin-CXFLoIsa.js → PdfMarkdownPlugin-DW2ej8Vk.js} +73 -6
- package/src/ui/dist/assets/{PdfViewerPlugin-BYTmz2fK.js → PdfViewerPlugin-CmlDxbhU.js} +103 -34
- package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +3627 -0
- package/src/ui/dist/assets/{SearchPlugin-CjWBI1O9.js → SearchPlugin-DAjQZPSv.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-DdOBU3-S.js → TextViewerPlugin-C-nVAZb_.js} +5 -4
- package/src/ui/dist/assets/{VNCViewer-B8HGgLwQ.js → VNCViewer-D7-dIYon.js} +10 -10
- package/src/ui/dist/assets/bot-C_G4WtNI.js +21 -0
- package/src/ui/dist/assets/branding/logo-rokid.png +0 -0
- package/src/ui/dist/assets/browser-BAcuE0Xj.js +2895 -0
- package/src/ui/dist/assets/{code-BWAY76JP.js → code-Cd7WfiWq.js} +1 -1
- package/src/ui/dist/assets/{file-content-C1NwU5oQ.js → file-content-B57zsL9y.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-CywslwB9.js → file-diff-panel-DVoheLFq.js} +1 -1
- package/src/ui/dist/assets/{file-socket-B4kzuOBQ.js → file-socket-B5kXFxZP.js} +1 -1
- package/src/ui/dist/assets/{image-D-NZM-6P.js → image-LLOjkMHF.js} +1 -1
- package/src/ui/dist/assets/{index-DGIYDuTv.css → index-BQG-1s2o.css} +40 -13
- package/src/ui/dist/assets/{index-DHZJ_0TI.js → index-C3r2iGrp.js} +12 -12
- package/src/ui/dist/assets/{index-7Chr1g9c.js → index-CLQauncb.js} +15050 -9561
- package/src/ui/dist/assets/index-Dxa2eYMY.js +25 -0
- package/src/ui/dist/assets/{index-BdM1Gqfr.js → index-hOUOWbW2.js} +2 -2
- package/src/ui/dist/assets/{monaco-Cb2uKKe6.js → monaco-BGGAEii3.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-DSw_D3RV.js → pdf-effect-queue-DlEr1_y5.js} +16 -1
- package/src/ui/dist/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
- package/src/ui/dist/assets/{popover-Bg72DGgT.js → popover-CWJbJuYY.js} +1 -1
- package/src/ui/dist/assets/{project-sync-Ce_0BglY.js → project-sync-CRJiucYO.js} +18 -77
- package/src/ui/dist/assets/select-CoHB7pvH.js +1690 -0
- package/src/ui/dist/assets/{sigma-DPaACDrh.js → sigma-D5aJWR8J.js} +1 -1
- package/src/ui/dist/assets/{index-CDxNdQdz.js → square-check-big-DUK_mnkS.js} +2 -13
- package/src/ui/dist/assets/{trash-BvTgE5__.js → trash-ChU3SEE3.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-CgPeMOwP.js → useCliAccess-BrJBV3tY.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-xPhz7P5B.js → useFileDiffOverlay-C2OQaVWc.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-C3Un3YQr.js → wrap-text-C7Qqh-om.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-BgxLa0Ri.js → zoom-out-rtX0FKya.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/src/ui/dist/assets/AutoFigurePlugin-BGxN8Umr.css +0 -3056
- package/src/ui/dist/assets/AutoFigurePlugin-C_wWw4AP.js +0 -8149
- package/src/ui/dist/assets/PdfViewerPlugin-BJXtIwj_.css +0 -260
- package/src/ui/dist/assets/Stepper-B0Dd8CxK.js +0 -158
- package/src/ui/dist/assets/bibtex-CKaefIN2.js +0 -189
- package/src/ui/dist/assets/file-utils-H2fjA46S.js +0 -109
- package/src/ui/dist/assets/message-square-BzjLiXir.js +0 -16
- package/src/ui/dist/assets/pdfjs-DU1YE8WO.js +0 -3
- package/src/ui/dist/assets/tooltip-C_mA6R0w.js +0 -108
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
|
+
from collections import deque
|
|
4
5
|
from contextlib import contextmanager
|
|
6
|
+
from datetime import UTC, datetime, timedelta
|
|
5
7
|
import hashlib
|
|
6
8
|
import subprocess
|
|
7
9
|
import json
|
|
@@ -19,11 +21,11 @@ except ImportError: # pragma: no cover
|
|
|
19
21
|
|
|
20
22
|
from ..artifact.metrics import build_metrics_timeline, extract_latest_metric
|
|
21
23
|
from ..config import ConfigManager
|
|
22
|
-
from ..connector_runtime import conversation_identity_key, normalize_conversation_id
|
|
24
|
+
from ..connector_runtime import conversation_identity_key, normalize_conversation_id, parse_conversation_id
|
|
23
25
|
from ..gitops import current_branch, export_git_graph, head_commit, init_repo
|
|
24
26
|
from ..home import repo_root
|
|
25
27
|
from ..registries import BaselineRegistry
|
|
26
|
-
from ..shared import append_jsonl, ensure_dir, generate_id, read_json, read_jsonl, read_text, read_yaml, resolve_within, run_command, sha256_text, slugify, utc_now, write_json, write_text, write_yaml
|
|
28
|
+
from ..shared import append_jsonl, ensure_dir, generate_id, iter_jsonl, read_json, read_jsonl, read_jsonl_tail, read_text, read_yaml, resolve_within, run_command, sha256_text, slugify, utc_now, write_json, write_text, write_yaml
|
|
27
29
|
from ..skills import SkillInstaller
|
|
28
30
|
from ..web_search import extract_web_search_payload
|
|
29
31
|
from .layout import (
|
|
@@ -42,6 +44,126 @@ _UNSET = object()
|
|
|
42
44
|
_NUMERIC_QUEST_ID_PATTERN = re.compile(r"^\d{1,10}$")
|
|
43
45
|
_MAX_NUMERIC_QUEST_ID_VALUE = 9_999_999_999
|
|
44
46
|
_NUMERIC_QUEST_ID_PAD_WIDTH = 3
|
|
47
|
+
_CRASH_AUTO_RESUME_WINDOW = timedelta(hours=24)
|
|
48
|
+
_JSONL_CACHE_MAX_BYTES = 4 * 1024 * 1024
|
|
49
|
+
_CODEX_HISTORY_TAIL_LIMIT = 400
|
|
50
|
+
_JSONL_STREAM_CHUNK_BYTES = 64 * 1024
|
|
51
|
+
_EVENTS_OVERSIZED_LINE_BYTES = 8 * 1024 * 1024
|
|
52
|
+
_OVERSIZED_EVENT_PREFIX_BYTES = 4096
|
|
53
|
+
_EVENT_TYPE_BYTES_RE = re.compile(rb'"(?:type|event_type)"\s*:\s*"([^"]+)"')
|
|
54
|
+
_EVENT_TOOL_NAME_BYTES_RE = re.compile(rb'"tool_name"\s*:\s*"([^"]+)"')
|
|
55
|
+
_EVENT_RUN_ID_BYTES_RE = re.compile(rb'"run_id"\s*:\s*"([^"]+)"')
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _oversized_event_placeholder(*, prefix: bytes, line_bytes: int) -> dict[str, Any]:
|
|
59
|
+
def _extract(pattern: re.Pattern[bytes]) -> str | None:
|
|
60
|
+
match = pattern.search(prefix)
|
|
61
|
+
if match is None:
|
|
62
|
+
return None
|
|
63
|
+
try:
|
|
64
|
+
return match.group(1).decode("utf-8", errors="ignore").strip() or None
|
|
65
|
+
except Exception:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
event_type = _extract(_EVENT_TYPE_BYTES_RE) or "runner.tool_result"
|
|
69
|
+
tool_name = _extract(_EVENT_TOOL_NAME_BYTES_RE)
|
|
70
|
+
run_id = _extract(_EVENT_RUN_ID_BYTES_RE)
|
|
71
|
+
summary = f"Omitted oversized quest event payload ({line_bytes} bytes) while reading event history."
|
|
72
|
+
payload: dict[str, Any] = {
|
|
73
|
+
"type": event_type,
|
|
74
|
+
"status": "omitted",
|
|
75
|
+
"summary": summary,
|
|
76
|
+
"oversized_event": True,
|
|
77
|
+
"oversized_bytes": line_bytes,
|
|
78
|
+
}
|
|
79
|
+
if tool_name:
|
|
80
|
+
payload["tool_name"] = tool_name
|
|
81
|
+
if run_id:
|
|
82
|
+
payload["run_id"] = run_id
|
|
83
|
+
return payload
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _iter_jsonl_records_safely(
|
|
87
|
+
path: Path,
|
|
88
|
+
*,
|
|
89
|
+
oversized_line_bytes: int = _EVENTS_OVERSIZED_LINE_BYTES,
|
|
90
|
+
):
|
|
91
|
+
if not path.exists():
|
|
92
|
+
return
|
|
93
|
+
with path.open("rb") as handle:
|
|
94
|
+
buffer = bytearray()
|
|
95
|
+
prefix = bytearray()
|
|
96
|
+
current_bytes = 0
|
|
97
|
+
oversized = False
|
|
98
|
+
while True:
|
|
99
|
+
chunk = handle.read(_JSONL_STREAM_CHUNK_BYTES)
|
|
100
|
+
if not chunk:
|
|
101
|
+
break
|
|
102
|
+
start = 0
|
|
103
|
+
while start <= len(chunk):
|
|
104
|
+
newline_index = chunk.find(b"\n", start)
|
|
105
|
+
has_newline = newline_index >= 0
|
|
106
|
+
segment = chunk[start:newline_index] if has_newline else chunk[start:]
|
|
107
|
+
|
|
108
|
+
if oversized:
|
|
109
|
+
current_bytes += len(segment)
|
|
110
|
+
if has_newline:
|
|
111
|
+
yield _oversized_event_placeholder(prefix=bytes(prefix), line_bytes=current_bytes)
|
|
112
|
+
prefix = bytearray()
|
|
113
|
+
current_bytes = 0
|
|
114
|
+
oversized = False
|
|
115
|
+
start = newline_index + 1
|
|
116
|
+
continue
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
next_bytes = current_bytes + len(segment)
|
|
120
|
+
if next_bytes > oversized_line_bytes:
|
|
121
|
+
combined_prefix = bytes(buffer)
|
|
122
|
+
remaining = max(0, _OVERSIZED_EVENT_PREFIX_BYTES - len(combined_prefix))
|
|
123
|
+
if remaining:
|
|
124
|
+
combined_prefix += segment[:remaining]
|
|
125
|
+
prefix = bytearray(combined_prefix)
|
|
126
|
+
buffer.clear()
|
|
127
|
+
current_bytes = next_bytes
|
|
128
|
+
oversized = True
|
|
129
|
+
if has_newline:
|
|
130
|
+
yield _oversized_event_placeholder(prefix=bytes(prefix), line_bytes=current_bytes)
|
|
131
|
+
prefix = bytearray()
|
|
132
|
+
current_bytes = 0
|
|
133
|
+
oversized = False
|
|
134
|
+
start = newline_index + 1
|
|
135
|
+
continue
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
buffer.extend(segment)
|
|
139
|
+
current_bytes = next_bytes
|
|
140
|
+
if has_newline:
|
|
141
|
+
raw = bytes(buffer).strip()
|
|
142
|
+
buffer.clear()
|
|
143
|
+
line_bytes = current_bytes
|
|
144
|
+
current_bytes = 0
|
|
145
|
+
if raw:
|
|
146
|
+
try:
|
|
147
|
+
payload = json.loads(raw)
|
|
148
|
+
except json.JSONDecodeError:
|
|
149
|
+
payload = None
|
|
150
|
+
if isinstance(payload, dict):
|
|
151
|
+
yield payload
|
|
152
|
+
start = newline_index + 1
|
|
153
|
+
continue
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
if oversized:
|
|
157
|
+
yield _oversized_event_placeholder(prefix=bytes(prefix), line_bytes=current_bytes)
|
|
158
|
+
elif buffer:
|
|
159
|
+
raw = bytes(buffer).strip()
|
|
160
|
+
if raw:
|
|
161
|
+
try:
|
|
162
|
+
payload = json.loads(raw)
|
|
163
|
+
except json.JSONDecodeError:
|
|
164
|
+
payload = None
|
|
165
|
+
if isinstance(payload, dict):
|
|
166
|
+
yield payload
|
|
45
167
|
|
|
46
168
|
|
|
47
169
|
class QuestService:
|
|
@@ -64,6 +186,35 @@ class QuestService:
|
|
|
64
186
|
def _quest_root(self, quest_id: str) -> Path:
|
|
65
187
|
return self.quests_root / quest_id
|
|
66
188
|
|
|
189
|
+
def _normalized_binding_sources(self, sources: list[Any] | None) -> list[str]:
|
|
190
|
+
local_present = False
|
|
191
|
+
external_source: str | None = None
|
|
192
|
+
for raw in sources or []:
|
|
193
|
+
normalized = self._normalize_binding_source(raw)
|
|
194
|
+
if not normalized:
|
|
195
|
+
continue
|
|
196
|
+
if normalized == "local:default":
|
|
197
|
+
local_present = True
|
|
198
|
+
continue
|
|
199
|
+
parsed = parse_conversation_id(normalized)
|
|
200
|
+
connector = str((parsed or {}).get("connector") or "").strip().lower()
|
|
201
|
+
if connector == "local":
|
|
202
|
+
local_present = True
|
|
203
|
+
continue
|
|
204
|
+
external_source = normalized
|
|
205
|
+
if external_source:
|
|
206
|
+
return ["local:default", external_source]
|
|
207
|
+
if local_present:
|
|
208
|
+
return ["local:default"]
|
|
209
|
+
return ["local:default"]
|
|
210
|
+
|
|
211
|
+
def _binding_sources_payload(self, quest_root: Path) -> dict[str, list[str]]:
|
|
212
|
+
bindings_path = quest_root / ".ds" / "bindings.json"
|
|
213
|
+
payload = read_json(bindings_path, {"sources": ["local:default"]})
|
|
214
|
+
raw_sources = payload.get("sources") if isinstance(payload, dict) else ["local:default"]
|
|
215
|
+
sources = self._normalized_binding_sources(raw_sources if isinstance(raw_sources, list) else ["local:default"])
|
|
216
|
+
return {"sources": sources}
|
|
217
|
+
|
|
67
218
|
def preferred_locale(self, quest_root: Path | None = None) -> str:
|
|
68
219
|
if quest_root is not None:
|
|
69
220
|
try:
|
|
@@ -738,7 +889,7 @@ class QuestService:
|
|
|
738
889
|
"last_artifact_interact_at": runtime_state.get("last_artifact_interact_at"),
|
|
739
890
|
"last_delivered_batch_id": runtime_state.get("last_delivered_batch_id"),
|
|
740
891
|
"last_delivered_at": runtime_state.get("last_delivered_at"),
|
|
741
|
-
"bound_conversations":
|
|
892
|
+
"bound_conversations": self._binding_sources_payload(quest_root).get("sources") or ["local:default"],
|
|
742
893
|
"created_at": quest_yaml.get("created_at"),
|
|
743
894
|
"updated_at": quest_yaml.get("updated_at"),
|
|
744
895
|
"branch": research_state.get("current_workspace_branch") or research_state.get("research_head_branch"),
|
|
@@ -779,21 +930,15 @@ class QuestService:
|
|
|
779
930
|
getattr(stat, "st_mtime_ns", int(stat.st_mtime * 1_000_000_000)),
|
|
780
931
|
stat.st_size,
|
|
781
932
|
)
|
|
933
|
+
if stat.st_size > _JSONL_CACHE_MAX_BYTES:
|
|
934
|
+
with self._jsonl_cache_lock:
|
|
935
|
+
self._jsonl_cache.pop(cache_key, None)
|
|
936
|
+
return read_jsonl(path)
|
|
782
937
|
with self._jsonl_cache_lock:
|
|
783
938
|
cached = self._jsonl_cache.get(cache_key)
|
|
784
939
|
if cached and cached.get("state") == state:
|
|
785
940
|
return cached.get("records") or []
|
|
786
|
-
items
|
|
787
|
-
for line in path.read_text(encoding="utf-8").splitlines():
|
|
788
|
-
line = line.strip()
|
|
789
|
-
if not line:
|
|
790
|
-
continue
|
|
791
|
-
try:
|
|
792
|
-
payload = json.loads(line)
|
|
793
|
-
except json.JSONDecodeError:
|
|
794
|
-
continue
|
|
795
|
-
if isinstance(payload, dict):
|
|
796
|
-
items.append(payload)
|
|
941
|
+
items = read_jsonl(path)
|
|
797
942
|
with self._jsonl_cache_lock:
|
|
798
943
|
self._jsonl_cache[cache_key] = {
|
|
799
944
|
"state": state,
|
|
@@ -801,6 +946,57 @@ class QuestService:
|
|
|
801
946
|
}
|
|
802
947
|
return items
|
|
803
948
|
|
|
949
|
+
@staticmethod
|
|
950
|
+
def _read_jsonl_cursor_slice(
|
|
951
|
+
path: Path,
|
|
952
|
+
*,
|
|
953
|
+
after: int = 0,
|
|
954
|
+
before: int | None = None,
|
|
955
|
+
limit: int = 200,
|
|
956
|
+
tail: bool = False,
|
|
957
|
+
) -> tuple[list[tuple[int, dict[str, Any]]], int, bool]:
|
|
958
|
+
normalized_limit = max(int(limit or 0), 0)
|
|
959
|
+
if not path.exists():
|
|
960
|
+
return [], 0, False
|
|
961
|
+
if normalized_limit <= 0:
|
|
962
|
+
total = sum(1 for _ in _iter_jsonl_records_safely(path))
|
|
963
|
+
return [], total, False
|
|
964
|
+
|
|
965
|
+
if before is not None:
|
|
966
|
+
stop_cursor = max(int(before) - 1, 0)
|
|
967
|
+
window: deque[tuple[int, dict[str, Any]]] = deque(maxlen=normalized_limit)
|
|
968
|
+
total = 0
|
|
969
|
+
for payload in _iter_jsonl_records_safely(path):
|
|
970
|
+
total += 1
|
|
971
|
+
if total >= before:
|
|
972
|
+
break
|
|
973
|
+
window.append((total, payload))
|
|
974
|
+
has_more = bool(window and window[0][0] > 1)
|
|
975
|
+
return list(window), total, has_more
|
|
976
|
+
|
|
977
|
+
if tail:
|
|
978
|
+
window = deque(maxlen=normalized_limit)
|
|
979
|
+
total = 0
|
|
980
|
+
for payload in _iter_jsonl_records_safely(path):
|
|
981
|
+
total += 1
|
|
982
|
+
window.append((total, payload))
|
|
983
|
+
has_more = total > len(window)
|
|
984
|
+
return list(window), total, has_more
|
|
985
|
+
|
|
986
|
+
collected: list[tuple[int, dict[str, Any]]] = []
|
|
987
|
+
total = 0
|
|
988
|
+
saw_more = False
|
|
989
|
+
normalized_after = max(int(after or 0), 0)
|
|
990
|
+
for payload in _iter_jsonl_records_safely(path):
|
|
991
|
+
total += 1
|
|
992
|
+
if total <= normalized_after:
|
|
993
|
+
continue
|
|
994
|
+
if len(collected) < normalized_limit:
|
|
995
|
+
collected.append((total, payload))
|
|
996
|
+
continue
|
|
997
|
+
saw_more = True
|
|
998
|
+
return collected, total, saw_more
|
|
999
|
+
|
|
804
1000
|
@staticmethod
|
|
805
1001
|
def _path_state(path: Path) -> tuple[int, int, int] | None:
|
|
806
1002
|
if not path.exists():
|
|
@@ -1093,7 +1289,7 @@ class QuestService:
|
|
|
1093
1289
|
"last_artifact_interact_at": runtime_state.get("last_artifact_interact_at"),
|
|
1094
1290
|
"last_delivered_batch_id": runtime_state.get("last_delivered_batch_id"),
|
|
1095
1291
|
"last_delivered_at": runtime_state.get("last_delivered_at"),
|
|
1096
|
-
"bound_conversations":
|
|
1292
|
+
"bound_conversations": self._binding_sources_payload(quest_root).get("sources") or ["local:default"],
|
|
1097
1293
|
"created_at": quest_yaml.get("created_at"),
|
|
1098
1294
|
"updated_at": quest_yaml.get("updated_at"),
|
|
1099
1295
|
"branch": current_branch(workspace_root),
|
|
@@ -1362,61 +1558,30 @@ class QuestService:
|
|
|
1362
1558
|
def bind_source(self, quest_id: str, source: str) -> dict:
|
|
1363
1559
|
quest_root = self._quest_root(quest_id)
|
|
1364
1560
|
bindings_path = quest_root / ".ds" / "bindings.json"
|
|
1365
|
-
bindings =
|
|
1561
|
+
bindings = self._binding_sources_payload(quest_root)
|
|
1366
1562
|
normalized_source = self._normalize_binding_source(source)
|
|
1367
|
-
|
|
1368
|
-
changed =
|
|
1369
|
-
replaced = False
|
|
1370
|
-
sources: list[str] = []
|
|
1371
|
-
for item in list(bindings.get("sources") or []):
|
|
1372
|
-
existing = self._normalize_binding_source(str(item))
|
|
1373
|
-
if conversation_identity_key(existing) == normalized_key:
|
|
1374
|
-
if not replaced:
|
|
1375
|
-
sources.append(normalized_source)
|
|
1376
|
-
replaced = True
|
|
1377
|
-
if existing != normalized_source:
|
|
1378
|
-
changed = True
|
|
1379
|
-
else:
|
|
1380
|
-
changed = True
|
|
1381
|
-
continue
|
|
1382
|
-
sources.append(existing)
|
|
1383
|
-
if existing != item:
|
|
1384
|
-
changed = True
|
|
1385
|
-
if not replaced:
|
|
1386
|
-
sources.append(normalized_source)
|
|
1387
|
-
changed = True
|
|
1563
|
+
next_sources = self._normalized_binding_sources([*(bindings.get("sources") or []), normalized_source])
|
|
1564
|
+
changed = list(bindings.get("sources") or []) != next_sources
|
|
1388
1565
|
if changed:
|
|
1389
|
-
bindings["sources"] =
|
|
1566
|
+
bindings["sources"] = next_sources
|
|
1390
1567
|
write_json(bindings_path, bindings)
|
|
1391
1568
|
return bindings
|
|
1392
1569
|
|
|
1393
1570
|
def binding_sources(self, quest_id: str) -> list[str]:
|
|
1394
1571
|
quest_root = self._quest_root(quest_id)
|
|
1395
|
-
|
|
1396
|
-
bindings = read_json(bindings_path, {"sources": ["local:default"]})
|
|
1397
|
-
sources = [self._normalize_binding_source(item) for item in (bindings.get("sources") or [])]
|
|
1398
|
-
return [item for item in sources if item]
|
|
1572
|
+
return list(self._binding_sources_payload(quest_root).get("sources") or ["local:default"])
|
|
1399
1573
|
|
|
1400
1574
|
def set_binding_sources(self, quest_id: str, sources: list[str]) -> dict:
|
|
1401
1575
|
quest_root = self._quest_root(quest_id)
|
|
1402
1576
|
bindings_path = quest_root / ".ds" / "bindings.json"
|
|
1403
|
-
|
|
1404
|
-
ordered: list[str] = []
|
|
1405
|
-
seen: set[str] = set()
|
|
1406
|
-
for item in normalized_sources:
|
|
1407
|
-
key = conversation_identity_key(item)
|
|
1408
|
-
if not item or key in seen:
|
|
1409
|
-
continue
|
|
1410
|
-
seen.add(key)
|
|
1411
|
-
ordered.append(item)
|
|
1412
|
-
payload = {"sources": ordered}
|
|
1577
|
+
payload = {"sources": self._normalized_binding_sources(sources)}
|
|
1413
1578
|
write_json(bindings_path, payload)
|
|
1414
1579
|
return payload
|
|
1415
1580
|
|
|
1416
1581
|
def unbind_source(self, quest_id: str, source: str) -> dict:
|
|
1417
1582
|
quest_root = self._quest_root(quest_id)
|
|
1418
1583
|
bindings_path = quest_root / ".ds" / "bindings.json"
|
|
1419
|
-
bindings =
|
|
1584
|
+
bindings = self._binding_sources_payload(quest_root)
|
|
1420
1585
|
normalized_source = self._normalize_binding_source(source)
|
|
1421
1586
|
normalized_key = conversation_identity_key(normalized_source)
|
|
1422
1587
|
changed = False
|
|
@@ -1429,8 +1594,11 @@ class QuestService:
|
|
|
1429
1594
|
sources.append(existing)
|
|
1430
1595
|
if existing != item:
|
|
1431
1596
|
changed = True
|
|
1597
|
+
normalized_sources = self._normalized_binding_sources(sources)
|
|
1598
|
+
if normalized_sources != list(bindings.get("sources") or []):
|
|
1599
|
+
changed = True
|
|
1432
1600
|
if changed:
|
|
1433
|
-
bindings["sources"] =
|
|
1601
|
+
bindings["sources"] = normalized_sources
|
|
1434
1602
|
write_json(bindings_path, bindings)
|
|
1435
1603
|
return bindings
|
|
1436
1604
|
|
|
@@ -1591,6 +1759,12 @@ class QuestService:
|
|
|
1591
1759
|
if not active_run_id and status != "running":
|
|
1592
1760
|
continue
|
|
1593
1761
|
previous_status = status or "running"
|
|
1762
|
+
last_transition_at = self._runtime_recovery_timestamp(runtime_state, quest_data)
|
|
1763
|
+
recoverable = self._runtime_recovery_eligible(
|
|
1764
|
+
previous_status=previous_status,
|
|
1765
|
+
active_run_id=active_run_id or None,
|
|
1766
|
+
last_transition_at=last_transition_at,
|
|
1767
|
+
)
|
|
1594
1768
|
self.update_runtime_state(
|
|
1595
1769
|
quest_root=quest_root,
|
|
1596
1770
|
status="stopped",
|
|
@@ -1601,6 +1775,8 @@ class QuestService:
|
|
|
1601
1775
|
f"Recovered quest from stale runtime state; previous status `{previous_status}`"
|
|
1602
1776
|
+ (f", abandoned run `{active_run_id}`." if active_run_id else ".")
|
|
1603
1777
|
)
|
|
1778
|
+
if recoverable:
|
|
1779
|
+
summary = f"{summary} Auto-resume is eligible within the 24-hour recovery window."
|
|
1604
1780
|
append_jsonl(
|
|
1605
1781
|
quest_root / ".ds" / "events.jsonl",
|
|
1606
1782
|
{
|
|
@@ -1609,6 +1785,8 @@ class QuestService:
|
|
|
1609
1785
|
"quest_id": quest_root.name,
|
|
1610
1786
|
"previous_status": previous_status,
|
|
1611
1787
|
"abandoned_run_id": active_run_id or None,
|
|
1788
|
+
"last_transition_at": last_transition_at,
|
|
1789
|
+
"recoverable": recoverable,
|
|
1612
1790
|
"status": "stopped",
|
|
1613
1791
|
"summary": summary,
|
|
1614
1792
|
"created_at": utc_now(),
|
|
@@ -1619,11 +1797,53 @@ class QuestService:
|
|
|
1619
1797
|
"quest_id": quest_root.name,
|
|
1620
1798
|
"previous_status": previous_status,
|
|
1621
1799
|
"abandoned_run_id": active_run_id or None,
|
|
1800
|
+
"last_transition_at": last_transition_at,
|
|
1801
|
+
"recoverable": recoverable,
|
|
1622
1802
|
"status": "stopped",
|
|
1623
1803
|
}
|
|
1624
1804
|
)
|
|
1625
1805
|
return reconciled
|
|
1626
1806
|
|
|
1807
|
+
@staticmethod
|
|
1808
|
+
def _parse_runtime_timestamp(value: Any) -> datetime | None:
|
|
1809
|
+
normalized = str(value or "").strip()
|
|
1810
|
+
if not normalized:
|
|
1811
|
+
return None
|
|
1812
|
+
candidate = normalized.replace("Z", "+00:00")
|
|
1813
|
+
try:
|
|
1814
|
+
parsed = datetime.fromisoformat(candidate)
|
|
1815
|
+
except ValueError:
|
|
1816
|
+
return None
|
|
1817
|
+
if parsed.tzinfo is None:
|
|
1818
|
+
parsed = parsed.replace(tzinfo=UTC)
|
|
1819
|
+
return parsed.astimezone(UTC)
|
|
1820
|
+
|
|
1821
|
+
def _runtime_recovery_timestamp(self, runtime_state: dict[str, Any], quest_data: dict[str, Any]) -> str | None:
|
|
1822
|
+
for candidate in (
|
|
1823
|
+
runtime_state.get("last_transition_at"),
|
|
1824
|
+
quest_data.get("updated_at"),
|
|
1825
|
+
quest_data.get("created_at"),
|
|
1826
|
+
):
|
|
1827
|
+
parsed = self._parse_runtime_timestamp(candidate)
|
|
1828
|
+
if parsed is None:
|
|
1829
|
+
continue
|
|
1830
|
+
return parsed.isoformat()
|
|
1831
|
+
return None
|
|
1832
|
+
|
|
1833
|
+
def _runtime_recovery_eligible(
|
|
1834
|
+
self,
|
|
1835
|
+
*,
|
|
1836
|
+
previous_status: str,
|
|
1837
|
+
active_run_id: str | None,
|
|
1838
|
+
last_transition_at: str | None,
|
|
1839
|
+
) -> bool:
|
|
1840
|
+
if previous_status != "running" and not str(active_run_id or "").strip():
|
|
1841
|
+
return False
|
|
1842
|
+
parsed = self._parse_runtime_timestamp(last_transition_at)
|
|
1843
|
+
if parsed is None:
|
|
1844
|
+
return False
|
|
1845
|
+
return datetime.now(UTC) - parsed <= _CRASH_AUTO_RESUME_WINDOW
|
|
1846
|
+
|
|
1627
1847
|
def history(self, quest_id: str, limit: int = 100) -> list[dict]:
|
|
1628
1848
|
return self._read_cached_jsonl(self._quest_root(quest_id) / ".ds" / "conversations" / "main.jsonl")[-limit:]
|
|
1629
1849
|
|
|
@@ -1729,40 +1949,37 @@ class QuestService:
|
|
|
1729
1949
|
limit: int = 200,
|
|
1730
1950
|
tail: bool = False,
|
|
1731
1951
|
) -> dict:
|
|
1732
|
-
|
|
1952
|
+
event_path = self._quest_root(quest_id) / ".ds" / "events.jsonl"
|
|
1733
1953
|
normalized_limit = max(limit, 0)
|
|
1734
1954
|
direction = "after"
|
|
1735
1955
|
if before is not None:
|
|
1736
1956
|
direction = "before"
|
|
1737
|
-
end = max(int(before) - 1, 0)
|
|
1738
|
-
start = max(end - normalized_limit, 0)
|
|
1739
|
-
sliced = records[start:end]
|
|
1740
1957
|
elif tail and normalized_limit > 0:
|
|
1741
1958
|
direction = "tail"
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1959
|
+
sliced_records, total_records, has_more = self._read_jsonl_cursor_slice(
|
|
1960
|
+
event_path,
|
|
1961
|
+
after=after,
|
|
1962
|
+
before=before,
|
|
1963
|
+
limit=normalized_limit,
|
|
1964
|
+
tail=tail,
|
|
1965
|
+
)
|
|
1747
1966
|
enriched = []
|
|
1748
|
-
for
|
|
1967
|
+
for cursor, item in sliced_records:
|
|
1749
1968
|
enriched.append(
|
|
1750
1969
|
{
|
|
1751
|
-
"cursor":
|
|
1752
|
-
"event_id": item.get("event_id") or f"evt-{quest_id}-{
|
|
1970
|
+
"cursor": cursor,
|
|
1971
|
+
"event_id": item.get("event_id") or f"evt-{quest_id}-{cursor}",
|
|
1753
1972
|
**item,
|
|
1754
1973
|
}
|
|
1755
1974
|
)
|
|
1756
1975
|
if before is not None:
|
|
1757
|
-
next_cursor =
|
|
1976
|
+
next_cursor = enriched[-1]["cursor"] if enriched else max(min(int(before or 0) - 1, total_records), 0)
|
|
1977
|
+
elif tail:
|
|
1978
|
+
next_cursor = total_records
|
|
1758
1979
|
else:
|
|
1759
|
-
next_cursor =
|
|
1980
|
+
next_cursor = enriched[-1]["cursor"] if enriched else max(int(after or 0), 0)
|
|
1760
1981
|
oldest_cursor = enriched[0]["cursor"] if enriched else None
|
|
1761
1982
|
newest_cursor = enriched[-1]["cursor"] if enriched else None
|
|
1762
|
-
if before is not None:
|
|
1763
|
-
has_more = start > 0
|
|
1764
|
-
else:
|
|
1765
|
-
has_more = start > 0 if tail else next_cursor < len(records)
|
|
1766
1983
|
return {
|
|
1767
1984
|
"quest_id": quest_id,
|
|
1768
1985
|
"cursor": next_cursor,
|
|
@@ -1824,6 +2041,12 @@ class QuestService:
|
|
|
1824
2041
|
resolved_selection = dict(selection or {})
|
|
1825
2042
|
selection_ref = str(resolved_selection.get("selection_ref") or "").strip()
|
|
1826
2043
|
selection_type = str(resolved_selection.get("selection_type") or "stage_node").strip() or None
|
|
2044
|
+
if (
|
|
2045
|
+
selection_type == "branch_node"
|
|
2046
|
+
and selection_ref
|
|
2047
|
+
and not str(resolved_selection.get("branch_name") or "").strip()
|
|
2048
|
+
):
|
|
2049
|
+
resolved_selection["branch_name"] = selection_ref
|
|
1827
2050
|
trace = None
|
|
1828
2051
|
if selection_ref:
|
|
1829
2052
|
try:
|
|
@@ -2080,7 +2303,18 @@ class QuestService:
|
|
|
2080
2303
|
if document_id.startswith(("questpath::", "memory::"))
|
|
2081
2304
|
else workspace_root
|
|
2082
2305
|
)
|
|
2083
|
-
|
|
2306
|
+
try:
|
|
2307
|
+
path, writable, scope, source_kind = self._resolve_document(resolution_root, document_id)
|
|
2308
|
+
except FileNotFoundError:
|
|
2309
|
+
legacy_relative = None
|
|
2310
|
+
if document_id.startswith("path::"):
|
|
2311
|
+
legacy_relative = document_id.split("::", 1)[1].lstrip("/")
|
|
2312
|
+
if legacy_relative and legacy_relative.startswith("literature/arxiv/"):
|
|
2313
|
+
path, writable, scope, source_kind = self._resolve_document(
|
|
2314
|
+
quest_root, f"questpath::{legacy_relative}"
|
|
2315
|
+
)
|
|
2316
|
+
else:
|
|
2317
|
+
raise
|
|
2084
2318
|
renderer_hint, mime_type = self._renderer_hint_for(path)
|
|
2085
2319
|
is_text = self._is_text_document(path, mime_type, renderer_hint)
|
|
2086
2320
|
content = read_text(path) if is_text else ""
|
|
@@ -3549,7 +3783,35 @@ def _tool_name(event: dict, item: dict) -> str:
|
|
|
3549
3783
|
return "tool"
|
|
3550
3784
|
|
|
3551
3785
|
|
|
3786
|
+
def _structured_text(value: object) -> str:
|
|
3787
|
+
if value is None:
|
|
3788
|
+
return ""
|
|
3789
|
+
if isinstance(value, str):
|
|
3790
|
+
return value.strip()
|
|
3791
|
+
try:
|
|
3792
|
+
return json.dumps(value, ensure_ascii=False, indent=2)
|
|
3793
|
+
except TypeError:
|
|
3794
|
+
return str(value)
|
|
3795
|
+
|
|
3796
|
+
|
|
3797
|
+
def _is_bash_exec_item(event: dict, item: dict) -> bool:
|
|
3798
|
+
server = str(item.get("server") or event.get("server") or "").strip()
|
|
3799
|
+
tool = str(item.get("tool") or event.get("tool") or "").strip()
|
|
3800
|
+
return server == "bash_exec" and tool == "bash_exec"
|
|
3801
|
+
|
|
3802
|
+
|
|
3552
3803
|
def _tool_args(event: dict, item: dict) -> str:
|
|
3804
|
+
if _is_bash_exec_item(event, item):
|
|
3805
|
+
for value in (
|
|
3806
|
+
item.get("arguments"),
|
|
3807
|
+
event.get("arguments"),
|
|
3808
|
+
item.get("input"),
|
|
3809
|
+
event.get("input"),
|
|
3810
|
+
):
|
|
3811
|
+
text = _structured_text(value)
|
|
3812
|
+
if text:
|
|
3813
|
+
return text
|
|
3814
|
+
return ""
|
|
3553
3815
|
for value in (
|
|
3554
3816
|
item.get("command"),
|
|
3555
3817
|
item.get("query"),
|
|
@@ -3569,6 +3831,21 @@ def _tool_args(event: dict, item: dict) -> str:
|
|
|
3569
3831
|
|
|
3570
3832
|
|
|
3571
3833
|
def _tool_output(event: dict, item: dict) -> str:
|
|
3834
|
+
if _is_bash_exec_item(event, item):
|
|
3835
|
+
for value in (
|
|
3836
|
+
item.get("result"),
|
|
3837
|
+
item.get("output"),
|
|
3838
|
+
item.get("content"),
|
|
3839
|
+
event.get("result"),
|
|
3840
|
+
event.get("output"),
|
|
3841
|
+
event.get("content"),
|
|
3842
|
+
item.get("aggregated_output"),
|
|
3843
|
+
event.get("aggregated_output"),
|
|
3844
|
+
):
|
|
3845
|
+
text = _structured_text(value)
|
|
3846
|
+
if text:
|
|
3847
|
+
return text
|
|
3848
|
+
return ""
|
|
3572
3849
|
for value in (
|
|
3573
3850
|
item.get("aggregated_output"),
|
|
3574
3851
|
item.get("changes"),
|
|
@@ -3611,17 +3888,25 @@ def _mcp_tool_metadata(*, quest_id: str, run_id: str, server: str, tool: str, it
|
|
|
3611
3888
|
for key in ("command", "workdir", "mode", "timeout_seconds", "comment"):
|
|
3612
3889
|
if key in arguments:
|
|
3613
3890
|
metadata[key] = arguments.get(key)
|
|
3891
|
+
if server == "bash_exec" and tool == "bash_exec" and isinstance(arguments.get("id"), str):
|
|
3892
|
+
metadata["bash_id"] = arguments.get("id")
|
|
3614
3893
|
result_payload = _mcp_result_payload(item)
|
|
3615
3894
|
if server == "bash_exec" and tool == "bash_exec":
|
|
3616
3895
|
for key in (
|
|
3617
3896
|
"bash_id",
|
|
3618
3897
|
"status",
|
|
3898
|
+
"command",
|
|
3899
|
+
"workdir",
|
|
3900
|
+
"cwd",
|
|
3901
|
+
"kind",
|
|
3902
|
+
"comment",
|
|
3619
3903
|
"started_at",
|
|
3620
3904
|
"finished_at",
|
|
3621
3905
|
"exit_code",
|
|
3622
3906
|
"stop_reason",
|
|
3623
3907
|
"last_progress",
|
|
3624
3908
|
"log_path",
|
|
3909
|
+
"watchdog_after_seconds",
|
|
3625
3910
|
):
|
|
3626
3911
|
if key in result_payload:
|
|
3627
3912
|
metadata[key] = result_payload.get(key)
|
|
@@ -3636,7 +3921,7 @@ def _parse_codex_history(history_root: Path, *, quest_id: str, run_id: str, skil
|
|
|
3636
3921
|
entries: list[dict] = []
|
|
3637
3922
|
known_tool_names: dict[str, str] = {}
|
|
3638
3923
|
|
|
3639
|
-
for raw in
|
|
3924
|
+
for raw in read_jsonl_tail(history_path, _CODEX_HISTORY_TAIL_LIMIT):
|
|
3640
3925
|
timestamp = raw.get("timestamp")
|
|
3641
3926
|
event = raw.get("event")
|
|
3642
3927
|
if not isinstance(event, dict):
|