@researai/deepscientist 1.5.2 → 1.5.3
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 +22 -0
- package/bin/ds.js +384 -0
- package/docs/en/00_QUICK_START.md +22 -0
- package/docs/zh/00_QUICK_START.md +22 -0
- package/install.sh +120 -4
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/artifact/service.py +1 -1
- package/src/deepscientist/bash_exec/monitor.py +23 -4
- package/src/deepscientist/bash_exec/runtime.py +3 -0
- package/src/deepscientist/bash_exec/service.py +132 -4
- package/src/deepscientist/bridges/base.py +10 -19
- package/src/deepscientist/channels/discord_gateway.py +25 -2
- package/src/deepscientist/channels/feishu_long_connection.py +41 -3
- package/src/deepscientist/channels/qq.py +524 -64
- package/src/deepscientist/channels/qq_gateway.py +22 -3
- package/src/deepscientist/channels/relay.py +429 -90
- package/src/deepscientist/channels/slack_socket.py +29 -5
- package/src/deepscientist/channels/telegram_polling.py +25 -2
- package/src/deepscientist/channels/whatsapp_local_session.py +32 -4
- package/src/deepscientist/cli.py +27 -0
- package/src/deepscientist/config/models.py +6 -40
- package/src/deepscientist/config/service.py +164 -155
- package/src/deepscientist/connector_profiles.py +346 -0
- package/src/deepscientist/connector_runtime.py +88 -43
- package/src/deepscientist/daemon/api/handlers.py +47 -10
- package/src/deepscientist/daemon/api/router.py +2 -2
- package/src/deepscientist/daemon/app.py +682 -218
- package/src/deepscientist/mcp/server.py +60 -7
- package/src/deepscientist/migration.py +114 -0
- package/src/deepscientist/prompts/builder.py +30 -3
- package/src/deepscientist/qq_profiles.py +186 -0
- package/src/prompts/connectors/qq.md +42 -2
- package/src/prompts/system.md +85 -5
- package/src/skills/analysis-campaign/SKILL.md +11 -5
- package/src/skills/baseline/SKILL.md +66 -31
- package/src/skills/decision/SKILL.md +1 -1
- package/src/skills/experiment/SKILL.md +11 -5
- 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 +1 -1
- package/src/skills/review/SKILL.md +1 -1
- package/src/skills/scout/SKILL.md +1 -1
- package/src/skills/write/SKILL.md +1 -1
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-CZpg376x.js → AiManusChatView-qzChi9uh.js} +14 -37
- package/src/ui/dist/assets/{AnalysisPlugin-CtHA22g3.js → AnalysisPlugin-CcC_-UqN.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-BSWmLMmF.js → AutoFigurePlugin-DD8LkJLe.js} +5 -5
- package/src/ui/dist/assets/{CliPlugin-CJ7jdm_s.js → CliPlugin-DJJFfVmW.js} +17 -110
- package/src/ui/dist/assets/{CodeEditorPlugin-DhInVGFf.js → CodeEditorPlugin-CrjkHNLh.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-D1n8S9r5.js → CodeViewerPlugin-obnD6G5R.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-C4XM_kqk.js → DocViewerPlugin-DB9SUQVd.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-W6kS9r6v.js → GitDiffViewerPlugin-DZLlNlD2.js} +1 -1
- package/src/ui/dist/assets/{ImageViewerPlugin-DPeUx_Oz.js → ImageViewerPlugin-BGwfDZ0Y.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-eAelUaub.js → LabCopilotPanel-dfLptQcR.js} +10 -10
- package/src/ui/dist/assets/{LabPlugin-BbOrBxKY.js → LabPlugin-CeGjAl3A.js} +1 -1
- package/src/ui/dist/assets/{LatexPlugin-C-HhkVXY.js → LatexPlugin-BBJ7kd1V.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-BDIzIBfh.js → MarkdownViewerPlugin-DKZi7BcB.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DAOJphwr.js → MarketplacePlugin-C_k-9jD0.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-BsoMvDoU.js → NotebookEditor-4R88_BMO.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-fiC7RtHf.js → PdfLoader-DwEFQLrw.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-C5OxZBFK.js → PdfMarkdownPlugin-D-jdsqF8.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-CAbxQebk.js → PdfViewerPlugin-CmeBGDY0.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-SE33Lb9B.js → SearchPlugin-Dlz2WKJ4.js} +1 -1
- package/src/ui/dist/assets/{Stepper-0Av7GfV7.js → Stepper-ClOgzWM3.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-Daf2gJDI.js → TextViewerPlugin-DDQWxibk.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-BKrMUIOX.js → VNCViewer-CJXT0Nm8.js} +9 -9
- package/src/ui/dist/assets/{bibtex-JBdOEe45.js → bibtex-DLr4Rtk4.js} +1 -1
- package/src/ui/dist/assets/{code-B0TDFCZz.js → code-DgKK408Y.js} +1 -1
- package/src/ui/dist/assets/{file-content-3YtrSacz.js → file-content-6HBqQnvQ.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-CJEg5OG1.js → file-diff-panel-Dhu0TbBM.js} +1 -1
- package/src/ui/dist/assets/{file-socket-CYQYdmB1.js → file-socket-CP3iwVZG.js} +1 -1
- package/src/ui/dist/assets/{file-utils-Cd1C9Ppl.js → file-utils-BsS-Aw68.js} +1 -1
- package/src/ui/dist/assets/{image-B33ctrvC.js → image-ByeK-Zcv.js} +1 -1
- package/src/ui/dist/assets/{index-BVXsmS7V.js → index-BLjo5--a.js} +9499 -8688
- package/src/ui/dist/assets/{index-BNQWqmJ2.js → index-BdsE0uRz.js} +11 -11
- package/src/ui/dist/assets/{index-9CLPVeZh.js → index-C-eX-N6A.js} +1 -1
- package/src/ui/dist/assets/{index-SwmFAld3.css → index-CuQhlrR-.css} +49 -2
- package/src/ui/dist/assets/{index-Buw_N1VQ.js → index-DyremSIv.js} +2 -2
- package/src/ui/dist/assets/{message-square-D0cUJ9yU.js → message-square-DnagiLnc.js} +1 -1
- package/src/ui/dist/assets/{monaco-UZLYkp2n.js → monaco-4kBFeprs.js} +1 -1
- package/src/ui/dist/assets/{popover-CTeiY-dK.js → popover-hRCXZzs2.js} +1 -1
- package/src/ui/dist/assets/{project-sync-Dbs01Xky.js → project-sync-O_85YuP6.js} +1 -1
- package/src/ui/dist/assets/{sigma-CM08S-xT.js → sigma-DvKopSnL.js} +1 -1
- package/src/ui/dist/assets/{tooltip-pDtzvU9p.js → tooltip-BmlPc6kc.js} +1 -1
- package/src/ui/dist/assets/{trash-YvPCP-da.js → trash-n-UvdZFR.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-Bavi74Ac.js → useCliAccess-WDd3_wIh.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-CVXY6oeg.js → useFileDiffOverlay-rXLIL2NF.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-Cf4flRW7.js → wrap-text-qIYQ4a_W.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-Hb0Z1YpT.js → zoom-out-fZXCEFsy.js} +1 -1
- package/src/ui/dist/index.html +2 -2
package/install.sh
CHANGED
|
@@ -258,8 +258,8 @@ prune_tree() {
|
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
build_ui() {
|
|
261
|
-
if
|
|
262
|
-
print_step "Using
|
|
261
|
+
if should_use_prebuilt_bundle "$1/src/ui" "$1/src/ui/dist" "index.html" "${DEEPSCIENTIST_FORCE_UI_BUILD:-}"; then
|
|
262
|
+
print_step "Using up-to-date web UI bundle from source tree"
|
|
263
263
|
return
|
|
264
264
|
fi
|
|
265
265
|
print_step "Building web UI in install tree"
|
|
@@ -274,8 +274,16 @@ install_root_runtime() {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
build_tui() {
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
local tui_entry=""
|
|
278
|
+
if [ -f "$1/src/tui/dist/index.js" ]; then
|
|
279
|
+
tui_entry="index.js"
|
|
280
|
+
elif [ -f "$1/src/tui/dist/index.cjs" ]; then
|
|
281
|
+
tui_entry="index.cjs"
|
|
282
|
+
elif [ -d "$1/src/tui/dist/components" ]; then
|
|
283
|
+
tui_entry="components"
|
|
284
|
+
fi
|
|
285
|
+
if [ -n "$tui_entry" ] && should_use_prebuilt_bundle "$1/src/tui" "$1/src/tui/dist" "$tui_entry" "${DEEPSCIENTIST_FORCE_TUI_BUILD:-}"; then
|
|
286
|
+
print_step "Using up-to-date TUI bundle from source tree"
|
|
279
287
|
return
|
|
280
288
|
fi
|
|
281
289
|
print_step "Building TUI in install tree"
|
|
@@ -284,6 +292,107 @@ build_tui() {
|
|
|
284
292
|
npm --prefix "$1/src/tui" prune --omit=dev --no-audit --no-fund
|
|
285
293
|
}
|
|
286
294
|
|
|
295
|
+
truthy_env() {
|
|
296
|
+
case "$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" in
|
|
297
|
+
1|true|yes|on)
|
|
298
|
+
return 0
|
|
299
|
+
;;
|
|
300
|
+
*)
|
|
301
|
+
return 1
|
|
302
|
+
;;
|
|
303
|
+
esac
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
should_use_prebuilt_bundle() {
|
|
307
|
+
local source_root="$1"
|
|
308
|
+
local dist_root="$2"
|
|
309
|
+
local dist_entry="$3"
|
|
310
|
+
local force_value="${4:-}"
|
|
311
|
+
|
|
312
|
+
if truthy_env "$force_value"; then
|
|
313
|
+
return 1
|
|
314
|
+
fi
|
|
315
|
+
|
|
316
|
+
if [ ! -e "$dist_root/$dist_entry" ]; then
|
|
317
|
+
return 1
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
local freshness_output=""
|
|
321
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
322
|
+
freshness_output="$(python3 - "$source_root" "$dist_root" <<'PY'
|
|
323
|
+
from pathlib import Path
|
|
324
|
+
import sys
|
|
325
|
+
|
|
326
|
+
source_root = Path(sys.argv[1])
|
|
327
|
+
dist_root = Path(sys.argv[2])
|
|
328
|
+
ignore_names = {"dist", "node_modules", ".git", "__pycache__"}
|
|
329
|
+
|
|
330
|
+
if not source_root.exists() or not dist_root.exists():
|
|
331
|
+
print("stale")
|
|
332
|
+
raise SystemExit(0)
|
|
333
|
+
|
|
334
|
+
def iter_files(root: Path):
|
|
335
|
+
for path in root.rglob("*"):
|
|
336
|
+
if not path.is_file():
|
|
337
|
+
continue
|
|
338
|
+
if any(part in ignore_names for part in path.relative_to(root).parts):
|
|
339
|
+
continue
|
|
340
|
+
yield path
|
|
341
|
+
|
|
342
|
+
source_mtime = 0.0
|
|
343
|
+
for path in iter_files(source_root):
|
|
344
|
+
source_mtime = max(source_mtime, path.stat().st_mtime)
|
|
345
|
+
|
|
346
|
+
dist_mtime = 0.0
|
|
347
|
+
for path in dist_root.rglob("*"):
|
|
348
|
+
if not path.is_file():
|
|
349
|
+
continue
|
|
350
|
+
dist_mtime = max(dist_mtime, path.stat().st_mtime)
|
|
351
|
+
|
|
352
|
+
print("fresh" if dist_mtime >= source_mtime else "stale")
|
|
353
|
+
PY
|
|
354
|
+
)"
|
|
355
|
+
elif command -v python >/dev/null 2>&1; then
|
|
356
|
+
freshness_output="$(python - "$source_root" "$dist_root" <<'PY'
|
|
357
|
+
from pathlib import Path
|
|
358
|
+
import sys
|
|
359
|
+
|
|
360
|
+
source_root = Path(sys.argv[1])
|
|
361
|
+
dist_root = Path(sys.argv[2])
|
|
362
|
+
ignore_names = {"dist", "node_modules", ".git", "__pycache__"}
|
|
363
|
+
|
|
364
|
+
if not source_root.exists() or not dist_root.exists():
|
|
365
|
+
print("stale")
|
|
366
|
+
raise SystemExit(0)
|
|
367
|
+
|
|
368
|
+
def iter_files(root: Path):
|
|
369
|
+
for path in root.rglob("*"):
|
|
370
|
+
if not path.is_file():
|
|
371
|
+
continue
|
|
372
|
+
if any(part in ignore_names for part in path.relative_to(root).parts):
|
|
373
|
+
continue
|
|
374
|
+
yield path
|
|
375
|
+
|
|
376
|
+
source_mtime = 0.0
|
|
377
|
+
for path in iter_files(source_root):
|
|
378
|
+
source_mtime = max(source_mtime, path.stat().st_mtime)
|
|
379
|
+
|
|
380
|
+
dist_mtime = 0.0
|
|
381
|
+
for path in dist_root.rglob("*"):
|
|
382
|
+
if not path.is_file():
|
|
383
|
+
continue
|
|
384
|
+
dist_mtime = max(dist_mtime, path.stat().st_mtime)
|
|
385
|
+
|
|
386
|
+
print("fresh" if dist_mtime >= source_mtime else "stale")
|
|
387
|
+
PY
|
|
388
|
+
)"
|
|
389
|
+
else
|
|
390
|
+
return 1
|
|
391
|
+
fi
|
|
392
|
+
|
|
393
|
+
[ "$freshness_output" = "fresh" ]
|
|
394
|
+
}
|
|
395
|
+
|
|
287
396
|
write_install_wrappers() {
|
|
288
397
|
local target="$1"
|
|
289
398
|
mkdir -p "$target/bin"
|
|
@@ -292,6 +401,10 @@ write_install_wrappers() {
|
|
|
292
401
|
#!/usr/bin/env bash
|
|
293
402
|
set -euo pipefail
|
|
294
403
|
SCRIPT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
404
|
+
HOME_DIR="\$(cd "\$SCRIPT_DIR/../.." && pwd)"
|
|
405
|
+
if [ -z "\${DEEPSCIENTIST_HOME:-}" ]; then
|
|
406
|
+
export DEEPSCIENTIST_HOME="\$HOME_DIR"
|
|
407
|
+
fi
|
|
295
408
|
NODE_BIN="\${DEEPSCIENTIST_NODE:-node}"
|
|
296
409
|
exec "\$NODE_BIN" "\$SCRIPT_DIR/ds.js" "\$@"
|
|
297
410
|
EOF
|
|
@@ -308,6 +421,9 @@ write_global_wrapper() {
|
|
|
308
421
|
cat >"$target_path" <<EOF
|
|
309
422
|
#!/usr/bin/env bash
|
|
310
423
|
set -euo pipefail
|
|
424
|
+
if [ -z "\${DEEPSCIENTIST_HOME:-}" ]; then
|
|
425
|
+
export DEEPSCIENTIST_HOME="$BASE_DIR"
|
|
426
|
+
fi
|
|
311
427
|
exec "$INSTALL_DIR/bin/$command_name" "\$@"
|
|
312
428
|
EOF
|
|
313
429
|
chmod +x "$target_path"
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -5187,7 +5187,7 @@ class ArtifactService:
|
|
|
5187
5187
|
return targets
|
|
5188
5188
|
|
|
5189
5189
|
def _connectors_config(self) -> dict[str, Any]:
|
|
5190
|
-
return ConfigManager(self.home).
|
|
5190
|
+
return ConfigManager(self.home).load_named_normalized("connectors")
|
|
5191
5191
|
|
|
5192
5192
|
@staticmethod
|
|
5193
5193
|
def _delivery_policy(connectors: dict[str, Any]) -> str:
|
|
@@ -202,6 +202,18 @@ def _terminate_process(process: subprocess.Popen[bytes], process_group_id: int |
|
|
|
202
202
|
process.kill()
|
|
203
203
|
|
|
204
204
|
|
|
205
|
+
def _terminate_process_force(process: subprocess.Popen[bytes], process_group_id: int | None) -> None:
|
|
206
|
+
if process.poll() is not None:
|
|
207
|
+
return
|
|
208
|
+
if isinstance(process_group_id, int) and process_group_id > 0:
|
|
209
|
+
try:
|
|
210
|
+
os.killpg(process_group_id, signal.SIGKILL)
|
|
211
|
+
except ProcessLookupError:
|
|
212
|
+
return
|
|
213
|
+
else:
|
|
214
|
+
process.kill()
|
|
215
|
+
|
|
216
|
+
|
|
205
217
|
def _drain_buffer(
|
|
206
218
|
buffer: str,
|
|
207
219
|
append_line,
|
|
@@ -350,12 +362,15 @@ def run_monitor(session_dir: Path) -> int:
|
|
|
350
362
|
},
|
|
351
363
|
)
|
|
352
364
|
progress = _parse_progress_marker(line)
|
|
365
|
+
output_updates: dict[str, Any] = {}
|
|
366
|
+
if stream not in {"system", "prompt"}:
|
|
367
|
+
output_updates = {"last_output_at": timestamp, "last_output_seq": seq}
|
|
353
368
|
if progress is not None:
|
|
354
369
|
progress.setdefault("ts", timestamp)
|
|
355
370
|
_atomic_write_json(progress_path, progress)
|
|
356
|
-
update_meta(last_progress=progress, latest_seq=seq)
|
|
371
|
+
update_meta(last_progress=progress, latest_seq=seq, **output_updates)
|
|
357
372
|
else:
|
|
358
|
-
update_meta(latest_seq=seq)
|
|
373
|
+
update_meta(latest_seq=seq, **output_updates)
|
|
359
374
|
|
|
360
375
|
master_fd: int | None = None
|
|
361
376
|
slave_fd: int | None = None
|
|
@@ -410,16 +425,20 @@ def run_monitor(session_dir: Path) -> int:
|
|
|
410
425
|
if not stop_requested and stop_request_path.exists():
|
|
411
426
|
request = read_json(stop_request_path, {}) or {}
|
|
412
427
|
stop_reason = str(request.get("reason") or "user_stop").strip() or "user_stop"
|
|
428
|
+
force_stop = bool(request.get("force"))
|
|
413
429
|
update_meta(
|
|
414
430
|
status="terminating",
|
|
415
431
|
stop_reason=stop_reason,
|
|
416
432
|
stopped_by_user_id=str(request.get("user_id") or meta.get("stopped_by_user_id") or meta.get("agent_id") or "agent"),
|
|
417
433
|
)
|
|
418
434
|
append_line(
|
|
419
|
-
f"
|
|
435
|
+
f"{'Force t' if force_stop else 'T'}ermination requested: {stop_reason}",
|
|
420
436
|
stream="system",
|
|
421
437
|
)
|
|
422
|
-
|
|
438
|
+
if force_stop:
|
|
439
|
+
_terminate_process_force(process, process_group_id)
|
|
440
|
+
else:
|
|
441
|
+
_terminate_process(process, process_group_id)
|
|
423
442
|
stop_requested = True
|
|
424
443
|
|
|
425
444
|
if deadline is not None and time.monotonic() >= deadline and process.poll() is None and not stop_requested:
|
|
@@ -390,6 +390,9 @@ class TerminalRuntime:
|
|
|
390
390
|
progress.setdefault("ts", timestamp)
|
|
391
391
|
_atomic_write_json(self.meta_path.parent / "progress.json", progress)
|
|
392
392
|
meta["last_progress"] = progress
|
|
393
|
+
if stream not in {"system", "prompt"}:
|
|
394
|
+
meta["last_output_at"] = timestamp
|
|
395
|
+
meta["last_output_seq"] = seq
|
|
393
396
|
meta["latest_seq"] = seq
|
|
394
397
|
meta["updated_at"] = timestamp
|
|
395
398
|
_atomic_write_json(self.meta_path, meta)
|
|
@@ -11,6 +11,7 @@ import sys
|
|
|
11
11
|
import tempfile
|
|
12
12
|
import threading
|
|
13
13
|
import time
|
|
14
|
+
from datetime import UTC, datetime
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from typing import Any
|
|
16
17
|
|
|
@@ -26,6 +27,7 @@ DEFAULT_LOG_TAIL_LIMIT = 200
|
|
|
26
27
|
DEFAULT_POLL_INTERVAL_SECONDS = 0.35
|
|
27
28
|
TERMINAL_STATUSES = {"completed", "failed", "terminated"}
|
|
28
29
|
DEFAULT_TERMINAL_SESSION_ID = "terminal-main"
|
|
30
|
+
BASH_WATCHDOG_AFTER_SECONDS = 1800
|
|
29
31
|
INPUT_ESCAPE_SEQUENCE_RE = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b[@-_]")
|
|
30
32
|
|
|
31
33
|
|
|
@@ -89,6 +91,48 @@ def _parse_progress_marker(line: str) -> dict[str, Any] | None:
|
|
|
89
91
|
return payload
|
|
90
92
|
|
|
91
93
|
|
|
94
|
+
def _parse_timestamp(value: object) -> datetime | None:
|
|
95
|
+
normalized = _normalize_string(value)
|
|
96
|
+
if not normalized:
|
|
97
|
+
return None
|
|
98
|
+
try:
|
|
99
|
+
parsed = datetime.fromisoformat(normalized)
|
|
100
|
+
except ValueError:
|
|
101
|
+
return None
|
|
102
|
+
if parsed.tzinfo is None:
|
|
103
|
+
parsed = parsed.replace(tzinfo=UTC)
|
|
104
|
+
return parsed.astimezone(UTC)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _age_seconds(value: object, *, now: datetime | None = None) -> int | None:
|
|
108
|
+
parsed = _parse_timestamp(value)
|
|
109
|
+
if parsed is None:
|
|
110
|
+
return None
|
|
111
|
+
current = now or datetime.now(UTC)
|
|
112
|
+
return max(0, int((current - parsed).total_seconds()))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _latest_timestamp(*values: object) -> str | None:
|
|
116
|
+
latest_raw: str | None = None
|
|
117
|
+
latest_dt: datetime | None = None
|
|
118
|
+
for value in values:
|
|
119
|
+
normalized = _normalize_string(value)
|
|
120
|
+
parsed = _parse_timestamp(normalized)
|
|
121
|
+
if parsed is None:
|
|
122
|
+
continue
|
|
123
|
+
if latest_dt is None or parsed >= latest_dt:
|
|
124
|
+
latest_dt = parsed
|
|
125
|
+
latest_raw = normalized
|
|
126
|
+
return latest_raw
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _compact_command(command: object, *, max_length: int = 140) -> str:
|
|
130
|
+
normalized = " ".join(str(command or "").split())
|
|
131
|
+
if len(normalized) <= max_length:
|
|
132
|
+
return normalized
|
|
133
|
+
return normalized[: max(0, max_length - 3)].rstrip() + "..."
|
|
134
|
+
|
|
135
|
+
|
|
92
136
|
class BashExecService:
|
|
93
137
|
def __init__(self, home: Path) -> None:
|
|
94
138
|
self.home = home
|
|
@@ -224,8 +268,45 @@ class BashExecService:
|
|
|
224
268
|
payload["status"] = _coerce_session_status(payload.get("status"))
|
|
225
269
|
payload["last_progress"] = payload.get("last_progress") or read_json(self.progress_path(quest_root, str(payload.get("bash_id") or "")), None)
|
|
226
270
|
payload["kind"] = str(payload.get("kind") or "exec")
|
|
271
|
+
payload = self._enrich_watchdog_fields(payload)
|
|
227
272
|
return payload
|
|
228
273
|
|
|
274
|
+
@staticmethod
|
|
275
|
+
def _enrich_watchdog_fields(payload: dict[str, Any]) -> dict[str, Any]:
|
|
276
|
+
current = datetime.now(UTC)
|
|
277
|
+
last_progress = payload.get("last_progress")
|
|
278
|
+
last_progress_at = None
|
|
279
|
+
if isinstance(last_progress, dict):
|
|
280
|
+
last_progress_at = _normalize_string(last_progress.get("ts")) or None
|
|
281
|
+
last_output_at = _normalize_string(payload.get("last_output_at")) or None
|
|
282
|
+
latest_signal_at = _latest_timestamp(last_output_at, last_progress_at, payload.get("started_at"))
|
|
283
|
+
payload["last_progress_at"] = last_progress_at
|
|
284
|
+
payload["run_age_seconds"] = _age_seconds(payload.get("started_at"), now=current)
|
|
285
|
+
payload["status_age_seconds"] = _age_seconds(payload.get("updated_at"), now=current)
|
|
286
|
+
payload["silent_seconds"] = _age_seconds(last_output_at or payload.get("started_at"), now=current)
|
|
287
|
+
payload["progress_age_seconds"] = _age_seconds(last_progress_at, now=current)
|
|
288
|
+
payload["latest_signal_at"] = latest_signal_at
|
|
289
|
+
payload["signal_age_seconds"] = _age_seconds(latest_signal_at, now=current)
|
|
290
|
+
payload["watchdog_after_seconds"] = BASH_WATCHDOG_AFTER_SECONDS
|
|
291
|
+
payload["watchdog_overdue"] = (
|
|
292
|
+
payload.get("status") in {"running", "terminating"}
|
|
293
|
+
and isinstance(payload.get("signal_age_seconds"), int)
|
|
294
|
+
and int(payload["signal_age_seconds"]) >= BASH_WATCHDOG_AFTER_SECONDS
|
|
295
|
+
)
|
|
296
|
+
return payload
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def format_history_line(session: dict[str, Any]) -> str:
|
|
300
|
+
timestamp = (
|
|
301
|
+
_normalize_string(session.get("started_at"))
|
|
302
|
+
or _normalize_string(session.get("updated_at"))
|
|
303
|
+
or _normalize_string(session.get("finished_at"))
|
|
304
|
+
or "unknown-time"
|
|
305
|
+
)
|
|
306
|
+
command = _compact_command(session.get("command"))
|
|
307
|
+
bash_id = _normalize_string(session.get("bash_id") or session.get("id")) or "unknown-id"
|
|
308
|
+
return f"{timestamp} | {command} | {bash_id}"
|
|
309
|
+
|
|
229
310
|
@staticmethod
|
|
230
311
|
def _summary_session_payload(meta: dict[str, Any]) -> dict[str, Any]:
|
|
231
312
|
return {
|
|
@@ -233,6 +314,7 @@ class BashExecService:
|
|
|
233
314
|
"command": meta.get("command"),
|
|
234
315
|
"kind": meta.get("kind") or "exec",
|
|
235
316
|
"label": meta.get("label"),
|
|
317
|
+
"comment": meta.get("comment"),
|
|
236
318
|
"workdir": meta.get("workdir"),
|
|
237
319
|
"status": _coerce_session_status(meta.get("status")),
|
|
238
320
|
"exit_code": meta.get("exit_code"),
|
|
@@ -241,6 +323,8 @@ class BashExecService:
|
|
|
241
323
|
"finished_at": meta.get("finished_at"),
|
|
242
324
|
"updated_at": meta.get("updated_at"),
|
|
243
325
|
"last_progress": meta.get("last_progress"),
|
|
326
|
+
"last_output_at": meta.get("last_output_at"),
|
|
327
|
+
"last_output_seq": meta.get("last_output_seq"),
|
|
244
328
|
}
|
|
245
329
|
|
|
246
330
|
@staticmethod
|
|
@@ -469,6 +553,7 @@ class BashExecService:
|
|
|
469
553
|
*,
|
|
470
554
|
limit: int = DEFAULT_LOG_TAIL_LIMIT,
|
|
471
555
|
before_seq: int | None = None,
|
|
556
|
+
after_seq: int | None = None,
|
|
472
557
|
order: str = "asc",
|
|
473
558
|
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
|
474
559
|
if not self.meta_path(quest_root, bash_id).exists():
|
|
@@ -487,10 +572,13 @@ class BashExecService:
|
|
|
487
572
|
else:
|
|
488
573
|
time.sleep(0.03)
|
|
489
574
|
entries = read_jsonl(self.log_path(quest_root, bash_id))
|
|
575
|
+
latest_seq = int(entries[-1].get("seq") or 0) if entries else 0
|
|
490
576
|
normalized_before = before_seq if isinstance(before_seq, int) and before_seq > 0 else None
|
|
577
|
+
normalized_after = after_seq if isinstance(after_seq, int) and after_seq >= 0 else None
|
|
578
|
+
if normalized_after is not None:
|
|
579
|
+
entries = [entry for entry in entries if int(entry.get("seq") or 0) > normalized_after]
|
|
491
580
|
if normalized_before is not None:
|
|
492
581
|
entries = [entry for entry in entries if int(entry.get("seq") or 0) < normalized_before]
|
|
493
|
-
latest_seq = int(entries[-1].get("seq") or 0) if entries else 0
|
|
494
582
|
normalized_limit = max(1, limit)
|
|
495
583
|
truncated = len(entries) > normalized_limit
|
|
496
584
|
selected = entries[-normalized_limit:]
|
|
@@ -501,6 +589,8 @@ class BashExecService:
|
|
|
501
589
|
"tail_limit": normalized_limit,
|
|
502
590
|
"tail_start_seq": tail_start_seq if truncated else tail_start_seq,
|
|
503
591
|
"latest_seq": latest_seq or None,
|
|
592
|
+
"after_seq": normalized_after,
|
|
593
|
+
"before_seq": normalized_before,
|
|
504
594
|
}
|
|
505
595
|
return selected, meta
|
|
506
596
|
|
|
@@ -521,7 +611,15 @@ class BashExecService:
|
|
|
521
611
|
return session
|
|
522
612
|
time.sleep(max(0.1, poll_interval))
|
|
523
613
|
|
|
524
|
-
def request_stop(
|
|
614
|
+
def request_stop(
|
|
615
|
+
self,
|
|
616
|
+
quest_root: Path,
|
|
617
|
+
bash_id: str,
|
|
618
|
+
*,
|
|
619
|
+
reason: str | None = None,
|
|
620
|
+
user_id: str | None = None,
|
|
621
|
+
force: bool = False,
|
|
622
|
+
) -> dict[str, Any]:
|
|
525
623
|
session = self.get_session(quest_root, bash_id)
|
|
526
624
|
status = _normalize_string(session.get("status")).lower()
|
|
527
625
|
if status in TERMINAL_STATUSES:
|
|
@@ -530,6 +628,7 @@ class BashExecService:
|
|
|
530
628
|
"reason": _normalize_string(reason) or "user_stop",
|
|
531
629
|
"user_id": _normalize_string(user_id) or _normalize_string(session.get("agent_id")) or "agent",
|
|
532
630
|
"requested_at": utc_now(),
|
|
631
|
+
"force": bool(force),
|
|
533
632
|
}
|
|
534
633
|
_atomic_write_json(self.stop_request_path(quest_root, bash_id), request_payload)
|
|
535
634
|
meta = read_json(self.meta_path(quest_root, bash_id), {})
|
|
@@ -540,12 +639,18 @@ class BashExecService:
|
|
|
540
639
|
self._write_meta(quest_root, bash_id, meta)
|
|
541
640
|
runtime = self._terminal_runtime_manager.get_runtime(quest_root, bash_id)
|
|
542
641
|
if runtime is not None:
|
|
543
|
-
runtime.stop(reason=request_payload["reason"], force=
|
|
642
|
+
runtime.stop(reason=request_payload["reason"], force=bool(force))
|
|
544
643
|
else:
|
|
545
644
|
process_group_id = meta.get("process_group_id")
|
|
645
|
+
process_pid = meta.get("process_pid")
|
|
546
646
|
if isinstance(process_group_id, int) and process_group_id > 0:
|
|
547
647
|
try:
|
|
548
|
-
os.killpg(process_group_id, signal.SIGTERM)
|
|
648
|
+
os.killpg(process_group_id, signal.SIGKILL if force else signal.SIGTERM)
|
|
649
|
+
except ProcessLookupError:
|
|
650
|
+
pass
|
|
651
|
+
elif isinstance(process_pid, int) and process_pid > 0:
|
|
652
|
+
try:
|
|
653
|
+
os.kill(process_pid, signal.SIGKILL if force else signal.SIGTERM)
|
|
549
654
|
except ProcessLookupError:
|
|
550
655
|
pass
|
|
551
656
|
return self._session_payload(quest_root, meta)
|
|
@@ -561,6 +666,7 @@ class BashExecService:
|
|
|
561
666
|
workdir_display: str,
|
|
562
667
|
timeout_seconds: int | None,
|
|
563
668
|
env_keys: list[str],
|
|
669
|
+
comment: str | dict[str, Any] | None = None,
|
|
564
670
|
kind: str = "exec",
|
|
565
671
|
) -> dict[str, Any]:
|
|
566
672
|
quest_root = context.require_quest_root().resolve()
|
|
@@ -582,6 +688,7 @@ class BashExecService:
|
|
|
582
688
|
"agent_instance_id": agent_instance_id,
|
|
583
689
|
"started_by_user_id": started_by_user_id,
|
|
584
690
|
"stopped_by_user_id": None,
|
|
691
|
+
"comment": comment,
|
|
585
692
|
"command": command,
|
|
586
693
|
"workdir": workdir_display,
|
|
587
694
|
"cwd": str(cwd),
|
|
@@ -592,6 +699,8 @@ class BashExecService:
|
|
|
592
699
|
"exit_code": None,
|
|
593
700
|
"stop_reason": None,
|
|
594
701
|
"last_progress": None,
|
|
702
|
+
"last_output_at": None,
|
|
703
|
+
"last_output_seq": None,
|
|
595
704
|
"started_at": timestamp,
|
|
596
705
|
"finished_at": None,
|
|
597
706
|
"updated_at": timestamp,
|
|
@@ -638,6 +747,7 @@ class BashExecService:
|
|
|
638
747
|
workdir: str | None = None,
|
|
639
748
|
env: dict[str, Any] | None = None,
|
|
640
749
|
timeout_seconds: int | None = None,
|
|
750
|
+
comment: str | dict[str, Any] | None = None,
|
|
641
751
|
) -> dict[str, Any]:
|
|
642
752
|
if not _normalize_string(command):
|
|
643
753
|
raise ValueError("command_required")
|
|
@@ -656,6 +766,7 @@ class BashExecService:
|
|
|
656
766
|
workdir_display=workdir_display,
|
|
657
767
|
timeout_seconds=timeout_seconds,
|
|
658
768
|
env_keys=sorted(env_payload),
|
|
769
|
+
comment=comment,
|
|
659
770
|
kind="exec",
|
|
660
771
|
)
|
|
661
772
|
self.terminal_log_path(quest_root, bash_id).touch()
|
|
@@ -668,6 +779,7 @@ class BashExecService:
|
|
|
668
779
|
"bash_id": bash_id,
|
|
669
780
|
"quest_id": meta["quest_id"],
|
|
670
781
|
"command": command,
|
|
782
|
+
"comment": comment,
|
|
671
783
|
"workdir": workdir_display,
|
|
672
784
|
"mode": mode,
|
|
673
785
|
"started_at": meta["started_at"],
|
|
@@ -725,6 +837,8 @@ class BashExecService:
|
|
|
725
837
|
"exit_code": None,
|
|
726
838
|
"stop_reason": None,
|
|
727
839
|
"last_progress": None,
|
|
840
|
+
"last_output_at": None,
|
|
841
|
+
"last_output_seq": None,
|
|
728
842
|
"started_at": timestamp,
|
|
729
843
|
"finished_at": None,
|
|
730
844
|
"updated_at": timestamp,
|
|
@@ -1054,6 +1168,9 @@ class BashExecService:
|
|
|
1054
1168
|
"bash_id": session["bash_id"],
|
|
1055
1169
|
"log_path": session.get("log_path"),
|
|
1056
1170
|
"status": session.get("status"),
|
|
1171
|
+
"kind": session.get("kind"),
|
|
1172
|
+
"comment": session.get("comment"),
|
|
1173
|
+
"label": session.get("label"),
|
|
1057
1174
|
"command": session.get("command"),
|
|
1058
1175
|
"workdir": session.get("workdir"),
|
|
1059
1176
|
"started_at": session.get("started_at"),
|
|
@@ -1061,6 +1178,17 @@ class BashExecService:
|
|
|
1061
1178
|
"exit_code": session.get("exit_code"),
|
|
1062
1179
|
"stop_reason": session.get("stop_reason"),
|
|
1063
1180
|
"last_progress": session.get("last_progress"),
|
|
1181
|
+
"last_progress_at": session.get("last_progress_at"),
|
|
1182
|
+
"last_output_at": session.get("last_output_at"),
|
|
1183
|
+
"last_output_seq": session.get("last_output_seq"),
|
|
1184
|
+
"run_age_seconds": session.get("run_age_seconds"),
|
|
1185
|
+
"status_age_seconds": session.get("status_age_seconds"),
|
|
1186
|
+
"silent_seconds": session.get("silent_seconds"),
|
|
1187
|
+
"progress_age_seconds": session.get("progress_age_seconds"),
|
|
1188
|
+
"latest_signal_at": session.get("latest_signal_at"),
|
|
1189
|
+
"signal_age_seconds": session.get("signal_age_seconds"),
|
|
1190
|
+
"watchdog_after_seconds": session.get("watchdog_after_seconds"),
|
|
1191
|
+
"watchdog_overdue": session.get("watchdog_overdue"),
|
|
1064
1192
|
}
|
|
1065
1193
|
if include_log:
|
|
1066
1194
|
result["log"] = self.read_terminal_log(quest_root, str(session["bash_id"]))
|
|
@@ -10,6 +10,8 @@ from urllib.error import URLError
|
|
|
10
10
|
from urllib.parse import parse_qs
|
|
11
11
|
from urllib.request import Request, urlopen
|
|
12
12
|
|
|
13
|
+
from ..connector_runtime import parse_conversation_id
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
@dataclass
|
|
15
17
|
class BridgeWebhookResult:
|
|
@@ -52,19 +54,6 @@ class BaseConnectorBridge:
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
def deliver(self, payload: dict[str, Any], config: dict[str, Any]) -> dict[str, Any] | None:
|
|
55
|
-
relay_url = str(config.get("relay_url") or "").strip()
|
|
56
|
-
if relay_url:
|
|
57
|
-
envelope = {
|
|
58
|
-
"bridge_version": "deepscientist-connector-bridge/v1",
|
|
59
|
-
"connector": self.name,
|
|
60
|
-
"payload": self.format_outbound(payload, config),
|
|
61
|
-
"normalized_payload": payload,
|
|
62
|
-
}
|
|
63
|
-
headers = {"Content-Type": "application/json; charset=utf-8"}
|
|
64
|
-
token = str(config.get("relay_auth_token") or "").strip()
|
|
65
|
-
if token:
|
|
66
|
-
headers["Authorization"] = f"Bearer {token}"
|
|
67
|
-
return self._post_json(relay_url, envelope, headers=headers)
|
|
68
57
|
return self.deliver_direct(payload, config)
|
|
69
58
|
|
|
70
59
|
def deliver_direct(self, payload: dict[str, Any], config: dict[str, Any]) -> dict[str, Any] | None:
|
|
@@ -72,18 +61,20 @@ class BaseConnectorBridge:
|
|
|
72
61
|
|
|
73
62
|
@staticmethod
|
|
74
63
|
def extract_target(conversation_id: Any) -> dict[str, str]:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if len(parts) == 3:
|
|
64
|
+
parsed = parse_conversation_id(conversation_id)
|
|
65
|
+
if parsed is not None:
|
|
78
66
|
return {
|
|
79
|
-
"connector":
|
|
80
|
-
"chat_type":
|
|
81
|
-
"chat_id":
|
|
67
|
+
"connector": str(parsed.get("connector") or ""),
|
|
68
|
+
"chat_type": str(parsed.get("chat_type") or ""),
|
|
69
|
+
"chat_id": str(parsed.get("chat_id") or ""),
|
|
70
|
+
"profile_id": str(parsed.get("profile_id") or ""),
|
|
82
71
|
}
|
|
72
|
+
raw = str(conversation_id or "").strip()
|
|
83
73
|
return {
|
|
84
74
|
"connector": "",
|
|
85
75
|
"chat_type": "",
|
|
86
76
|
"chat_id": raw,
|
|
77
|
+
"profile_id": "",
|
|
87
78
|
}
|
|
88
79
|
|
|
89
80
|
@staticmethod
|