dsh-adb 1.5.0 → 1.6.1

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 CHANGED
@@ -20,7 +20,7 @@ A "设备" tab in the conversation view ring (next to chat / trajectory / automa
20
20
 
21
21
  ## Ecosystem
22
22
 
23
- - ✅ [npm](https://www.npmjs.com/package/dsh-adb) — `dsh-adb` published (latest: 1.1.0)
23
+ - ✅ [npm](https://www.npmjs.com/package/dsh-adb) — `dsh-adb` published (latest: 1.6.0)
24
24
  - ✅ [awesome-deepseek-harness#87](https://github.com/0xsline/awesome-deepseek-harness/pull/87) — **merged**
25
25
  - ✅ [awesome-dsh-plugin#85](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/85) — **merged**
26
26
  - ✅ [awesome-DSH-plugin#29](https://github.com/Alex-Yanggg/awesome-DSH-plugin/pull/29) — **merged**
@@ -43,9 +43,57 @@ Topics: `dsh-plugin` `dsh` `adb` `android` `automotive` `bench`
43
43
  | `adb_wait_for` | Wait until a device condition holds — device-online / boot-complete / process appeared / logcat keyword — polling up to a budget, instead of sleeping a fixed number of seconds; returns `matched:false` on timeout |
44
44
  | `adb_operation_ledger` | Append-only device operation ledger (record/list/rollback): record installs/pushes/etc., list history, or roll an app back to its last known-good APK (`adb install -r`); persisted as `operations.json` — the trust base for agent-driven device modification |
45
45
  | `adb_screenshot` | Capture the device screen into a local PNG (screencap → pull), returning the saved path, byte size, and pixel dimensions — durable evidence for crash scenes / UI states / test frames |
46
+ | `adb_watch_crash` | Watch the crash buffer for NEW real crashes (foreground poll or background job; boot markers ignored; `matched:false` on timeout) — the first link of the monitor→capture→attribute chain |
46
47
 
47
48
  Errors are structured `AdbError` with stable codes: `ADB_NOT_FOUND`, `ADB_UNAVAILABLE`, `DEVICE_NOT_FOUND`, `NO_DEVICES`, `CONNECT_FAILED`, `INSTALL_FAILED`, `ADB_EXIT_<code>`, etc.
48
49
 
50
+ ## Usage: adb_watch_crash (Crash Watchdog)
51
+
52
+ Watch the device's crash buffer for **new** real crashes. On start, it reads the current buffer and remembers every existing crash signature (seed), so only crashes that appear *after* the watch begins are reported. Boot markers (`mtk-brm-*`) are not crashes and are ignored.
53
+
54
+ **Foreground mode** (default) — blocks until a new crash appears or the budget expires:
55
+
56
+ ```json
57
+ // Agent calls:
58
+ { "name": "adb_watch_crash", "args": { "timeoutMs": 60000 } }
59
+
60
+ // New crash detected:
61
+ { "matched": true, "waitedMs": 3200, "crashes": [{ "time": "08-26 06:22:56.714", "pid": "2947", "tag": "AndroidRuntime", "message": "FATAL EXCEPTION: WM.task-1", "stack": ["AndroidRuntime: Process: com.miui.weather2, PID: 2947", "..."] }] }
62
+
63
+ // No new crash within budget (not an error):
64
+ { "matched": false, "waitedMs": 60000, "crashes": [], "reason": "no new crash within 60000ms" }
65
+ ```
66
+
67
+ **Background mode** (`run_in_background`) — returns a job id immediately, keeps polling:
68
+
69
+ ```json
70
+ // Start watching in the background:
71
+ { "name": "adb_watch_crash", "args": { "run_in_background": true, "timeoutMs": 300000 } }
72
+ // → { "kind": "background", "jobId": "adb-watch-crash-1" }
73
+
74
+ // Read latest detections with job_output:
75
+ // [adb_watch_crash] detected 1 new crash(es) after 5400ms
76
+ // - 08-26 06:22:56.714 pid=2947 AndroidRuntime: FATAL EXCEPTION: WM.task-1
77
+ // AndroidRuntime: Process: com.miui.weather2, PID: 2947
78
+ // ...
79
+ ```
80
+
81
+ **Chain with crash capture** — when a crash is detected, pair with `adb_crash_report` and `adb_screenshot` to capture the scene:
82
+
83
+ ```
84
+ 1. adb_watch_crash (watch for new crash)
85
+ 2. → crash detected → adb_crash_report (full crash scene: buffer + dropbox + process + memory)
86
+ 3. → adb_screenshot (screen state at crash time)
87
+ ```
88
+
89
+ | Parameter | Type | Default | Description |
90
+ | --- | --- | --- | --- |
91
+ | `serial` | string | defaultSerial | Target device serial |
92
+ | `timeoutMs` | integer | 60000 | Watch budget (max 600000 = 10 min) |
93
+ | `intervalMs` | integer | 1000 | Poll interval (min 250) |
94
+ | `withStacks` | boolean | true | Include same-pid stack lines per crash |
95
+ | `run_in_background` | boolean | false | Run as a background job; read via `job_output` |
96
+
49
97
  ## Configuration
50
98
 
51
99
  Set the `config` block in `cordis.patch.yml` (or a profile patch):
@@ -80,7 +128,7 @@ npm pack --dry-run # verify publish contents (lib/ + cordis.patch.yml)
80
128
  ## Testing & Verification
81
129
 
82
130
  - Principle: **ship only what is tested** — every committed feature has unit and/or end-to-end coverage.
83
- - Verified on: Android 13 automotive bench + Android 13 phone.
131
+ - Verified on: Android 13 automotive bench + Android 14 phone (Redmi K50 Pro) + emulator.
84
132
  - Per-version changes and verification: [CHANGELOG.md](CHANGELOG.md); test methodology and coverage: [docs/TESTING.md](docs/TESTING.md) (Chinese).
85
133
 
86
134
  ## Project Docs (bilingual; for AI agents & contributors)
package/README.zh-CN.md CHANGED
@@ -20,7 +20,7 @@ dsh plugin --profile web add dsh-adb
20
20
 
21
21
  ## 生态收录
22
22
 
23
- - ✅ [npm](https://www.npmjs.com/package/dsh-adb) — `dsh-adb` 已发布(latest: 1.1.0)
23
+ - ✅ [npm](https://www.npmjs.com/package/dsh-adb) — `dsh-adb` 已发布(latest: 1.6.0)
24
24
  - ✅ [awesome-deepseek-harness#87](https://github.com/0xsline/awesome-deepseek-harness/pull/87) — **已合并**
25
25
  - ✅ [awesome-dsh-plugin#85](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/85) — **已合并**
26
26
  - ✅ [awesome-DSH-plugin#29](https://github.com/Alex-Yanggg/awesome-DSH-plugin/pull/29) — **已合并**
@@ -43,9 +43,57 @@ Topics:`dsh-plugin` `dsh` `adb` `android` `automotive` `bench`
43
43
  | `adb_wait_for` | 等待原语:等设备上线 / 启动完成 / 进程出现 / logcat 出现关键字,轮询到预算上限,替代盲目 sleep;超时返回 `matched:false` |
44
44
  | `adb_operation_ledger` | 操作回滚台账(record/list/rollback):追加记录安装/推送等操作,查询历史,或回滚到最近一次成功安装的 APK(`adb install -r`);落盘 `operations.json` —— agent 自主改设备的信任基础 |
45
45
  | `adb_screenshot` | 截图落盘 PNG(screencap → pull),返回路径/字节数/像素尺寸 —— 崩溃现场、UI 状态、测试前后对比的持久证据 |
46
+ | `adb_watch_crash` | 崩溃看门狗:监控 crash buffer 的**新**真实崩溃(前台轮询或后台 job;启动标记不算;超时返回 `matched:false`)—— 「盯崩溃→采集→归因」链第一环 |
46
47
 
47
48
  错误码:`ADB_NOT_FOUND`、`ADB_UNAVAILABLE`、`DEVICE_NOT_FOUND`、`NO_DEVICES`、`CONNECT_FAILED`、`INSTALL_FAILED`、`ADB_EXIT_<code>` 等,均为结构化 `AdbError`。
48
49
 
50
+ ## 用法:adb_watch_crash(崩溃看门狗)
51
+
52
+ 监控设备 crash buffer 里的**新**真实崩溃。启动时先读一遍当前 buffer,记住所有已有崩溃签名(seed),所以只报 watch 开始**之后**新出现的崩溃。启动标记(`mtk-brm-*`)不算崩溃,自动忽略。
53
+
54
+ **前台模式**(默认)——阻塞到新崩溃出现或预算耗尽:
55
+
56
+ ```json
57
+ // Agent 调用:
58
+ { "name": "adb_watch_crash", "args": { "timeoutMs": 60000 } }
59
+
60
+ // 检测到新崩溃:
61
+ { "matched": true, "waitedMs": 3200, "crashes": [{ "time": "08-26 06:22:56.714", "pid": "2947", "tag": "AndroidRuntime", "message": "FATAL EXCEPTION: WM.task-1", "stack": ["AndroidRuntime: Process: com.miui.weather2, PID: 2947", "..."] }] }
62
+
63
+ // 预算内无新崩溃(不是错误):
64
+ { "matched": false, "waitedMs": 60000, "crashes": [], "reason": "no new crash within 60000ms" }
65
+ ```
66
+
67
+ **后台模式**(`run_in_background`)——立即返回 job id,持续轮询:
68
+
69
+ ```json
70
+ // 启动后台监控:
71
+ { "name": "adb_watch_crash", "args": { "run_in_background": true, "timeoutMs": 300000 } }
72
+ // → { "kind": "background", "jobId": "adb-watch-crash-1" }
73
+
74
+ // 用 job_output 读最新检测结果:
75
+ // [adb_watch_crash] detected 1 new crash(es) after 5400ms
76
+ // - 08-26 06:22:56.714 pid=2947 AndroidRuntime: FATAL EXCEPTION: WM.task-1
77
+ // AndroidRuntime: Process: com.miui.weather2, PID: 2947
78
+ // ...
79
+ ```
80
+
81
+ **与现场采集衔接**——检测到崩溃后,配 `adb_crash_report` 和 `adb_screenshot` 抓现场:
82
+
83
+ ```
84
+ 1. adb_watch_crash(盯新崩溃)
85
+ 2. → 崩溃出现 → adb_crash_report(采集完整现场:buffer + dropbox + 进程 + 内存)
86
+ 3. → adb_screenshot(截取崩溃时的屏幕状态)
87
+ ```
88
+
89
+ | 参数 | 类型 | 默认 | 说明 |
90
+ | --- | --- | --- | --- |
91
+ | `serial` | string | defaultSerial | 目标设备 serial |
92
+ | `timeoutMs` | integer | 60000 | 监控预算(上限 600000 = 10 分钟) |
93
+ | `intervalMs` | integer | 1000 | 轮询间隔(最小 250) |
94
+ | `withStacks` | boolean | true | 是否包含同 pid 堆栈行 |
95
+ | `run_in_background` | boolean | false | 后台 job 模式,用 `job_output` 读结果 |
96
+
49
97
  ## 配置
50
98
 
51
99
  `cordis.patch.yml` 的 `config` 块(或 profile patch):
@@ -82,7 +130,7 @@ npm pack --dry-run # 校验发布包内容(lib/ + cordis.patch.yml)
82
130
  ## 测试与验证
83
131
 
84
132
  - 原则:**提交即测** —— 全部已提交功能均有实测覆盖(单元 + headless 端到端 + 车机台架/真机)。
85
- - 验证设备:Android 13 车机台架 + Android 13 真机。
133
+ - 验证设备:Android 13 车机台架 + Android 14 真机(Redmi K50 Pro)+ 模拟器。
86
134
  - 版本化变更与每版验证记录见 [CHANGELOG.md](CHANGELOG.md);测试方法与覆盖现状见 [docs/TESTING.md](docs/TESTING.md)。
87
135
 
88
136
  ## 项目文档(双语,供 AI 对话/协作者参考)
package/lib/index.js CHANGED
@@ -11,6 +11,7 @@ import { registerPerfBaselineTool } from './tools/perf-baseline.js';
11
11
  import { registerWaitTool } from './tools/wait.js';
12
12
  import { registerOperationLedgerTool } from './tools/operation-ledger.js';
13
13
  import { registerScreenshotTool } from './tools/screenshot.js';
14
+ import { registerWatchCrashTool } from './tools/watch-crash.js';
14
15
  import { registerRpc } from './rpc.js';
15
16
  import { registerSkills } from './skill.js';
16
17
  export const name = 'dsh-adb';
@@ -40,10 +41,11 @@ export function apply(ctx, config) {
40
41
  registerWaitTool(ctx, cfg);
41
42
  registerOperationLedgerTool(ctx, cfg, baselineDir);
42
43
  registerScreenshotTool(ctx, cfg, screenshotDir);
44
+ registerWatchCrashTool(ctx, cfg);
43
45
  registerSkills(ctx);
44
46
  // The RPC channel needs the client connection, which mounts after this
45
47
  // plugin starts in web compositions; register lazily so headless profiles
46
48
  // (no connection) stay unaffected.
47
49
  ctx.inject(['connection'], (readyCtx) => registerRpc(readyCtx, cfg, reportDir));
48
- ctx.logger.info('[dsh-adb] loaded: 13 tools + web device panel rpc');
50
+ ctx.logger.info('[dsh-adb] loaded: 14 tools + web device panel rpc');
49
51
  }
@@ -0,0 +1,46 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type AdbConfig } from '../adb.js';
3
+ import { parseLogcat } from '../parsers/logcat.js';
4
+ import { type CrashChain } from '../report.js';
5
+ /**
6
+ * adb_watch_crash: watch the crash buffer for NEW real crashes. Foreground:
7
+ * poll until a new real crash appears or the budget expires (returns
8
+ * matched:false on timeout, not an error). Background: run as a job that keeps
9
+ * polling; job_output returns a text summary of each newly detected crash.
10
+ *
11
+ * Reuses the crash classification from the health report (real crash vs. boot
12
+ * markers), so "a new crash" means a real one — not another mtk-brm line.
13
+ */
14
+ export interface WatchCrashArgs {
15
+ serial?: string;
16
+ /** Overall watch budget in milliseconds; defaults to 60000, capped at 600000. */
17
+ timeoutMs?: number;
18
+ /** Poll interval in milliseconds; defaults to 1000. */
19
+ intervalMs?: number;
20
+ /** Include the following same-pid lines (stack) per crash in the result. */
21
+ withStacks?: boolean;
22
+ run_in_background?: boolean;
23
+ }
24
+ export interface CrashDetection {
25
+ matched: boolean;
26
+ waitedMs: number;
27
+ crashes: Array<{
28
+ time: string;
29
+ pid: string;
30
+ tag: string;
31
+ message: string;
32
+ stack: string[];
33
+ }>;
34
+ }
35
+ /** A stable identity for one crash occurrence: time+pid+tag+message. */
36
+ export declare function crashSignature(chain: CrashChain): string;
37
+ /** Map crash chains to the public detection shape. */
38
+ export declare function chainToDetection(chain: CrashChain, withStacks: boolean): CrashDetection['crashes'][number];
39
+ /** One poll: return chains not seen in `seen`, and update `seen`. */
40
+ export declare function detectNewCrashes(entries: ReturnType<typeof parseLogcat>, seen: Set<string>, withStacks: boolean): CrashDetection['crashes'];
41
+ /** Poll the crash buffer until a new real crash appears or the budget expires. */
42
+ export declare function watchForCrash(ctx: Context, cfg: AdbConfig, signal: AbortSignal, args: WatchCrashArgs): Promise<CrashDetection>;
43
+ /** Format detected crashes as a job_output-friendly text block. */
44
+ export declare function formatCrashDetections(detection: CrashDetection): string;
45
+ /** adb_watch_crash: watch the crash buffer for NEW real crashes (foreground poll or background job). */
46
+ export declare function registerWatchCrashTool(ctx: Context, cfg: AdbConfig): void;
@@ -0,0 +1,199 @@
1
+ import { AdbError, classifyFailure, jsonOutput, runAdb } from '../adb.js';
2
+ import { parseLogcat } from '../parsers/logcat.js';
3
+ import { classifyCrashBuffer } from '../report.js';
4
+ const DEFAULT_TIMEOUT_MS = 60_000;
5
+ const MAX_TIMEOUT_MS = 600_000;
6
+ const DEFAULT_INTERVAL_MS = 1_000;
7
+ /** A stable identity for one crash occurrence: time+pid+tag+message. */
8
+ export function crashSignature(chain) {
9
+ const sig = chain.signature;
10
+ return `${sig.time}|${sig.pid}|${sig.tag}|${sig.message}`;
11
+ }
12
+ /** Map crash chains to the public detection shape. */
13
+ export function chainToDetection(chain, withStacks) {
14
+ return {
15
+ time: chain.signature.time,
16
+ pid: chain.signature.pid,
17
+ tag: chain.signature.tag,
18
+ message: chain.signature.message,
19
+ stack: withStacks ? chain.following.map((entry) => `${entry.tag}: ${entry.message}`) : [],
20
+ };
21
+ }
22
+ /** One poll: return chains not seen in `seen`, and update `seen`. */
23
+ export function detectNewCrashes(entries, seen, withStacks) {
24
+ const summary = classifyCrashBuffer(entries);
25
+ const fresh = [];
26
+ for (const chain of summary.chains) {
27
+ const key = crashSignature(chain);
28
+ if (seen.has(key))
29
+ continue;
30
+ seen.add(key);
31
+ fresh.push(chainToDetection(chain, withStacks));
32
+ }
33
+ return fresh;
34
+ }
35
+ function sleep(ms, signal) {
36
+ return new Promise((resolve, reject) => {
37
+ if (signal?.aborted) {
38
+ reject(new Error('tool call aborted'));
39
+ return;
40
+ }
41
+ const timer = setTimeout(resolve, ms);
42
+ signal?.addEventListener('abort', () => {
43
+ clearTimeout(timer);
44
+ reject(new Error('tool call aborted'));
45
+ }, { once: true });
46
+ });
47
+ }
48
+ /** Poll the crash buffer until a new real crash appears or the budget expires. */
49
+ export async function watchForCrash(ctx, cfg, signal, args) {
50
+ const timeoutMs = Math.min(Math.floor(args.timeoutMs ?? DEFAULT_TIMEOUT_MS), MAX_TIMEOUT_MS);
51
+ const intervalMs = Math.max(Math.floor(args.intervalMs ?? DEFAULT_INTERVAL_MS), 250);
52
+ const start = Date.now();
53
+ const deadline = start + timeoutMs;
54
+ const seen = new Set();
55
+ const withStacks = args.withStacks ?? true;
56
+ let lastError;
57
+ // Seed: remember every crash already in the buffer, so only NEW ones match.
58
+ try {
59
+ const seed = await runAdb(ctx, cfg, ['logcat', '-b', 'crash', '-v', 'threadtime', '-d'], { signal, serial: args.serial, maxBytes: 8 * 1024 * 1024 });
60
+ if (seed.exitCode !== 0)
61
+ throw classifyFailure(seed);
62
+ for (const chain of classifyCrashBuffer(parseLogcat(seed.stdout)).chains) {
63
+ seen.add(crashSignature(chain));
64
+ }
65
+ }
66
+ catch (error) {
67
+ lastError = error instanceof Error ? error.message : String(error);
68
+ }
69
+ while (true) {
70
+ try {
71
+ const result = await runAdb(ctx, cfg, ['logcat', '-b', 'crash', '-v', 'threadtime', '-d'], { signal, serial: args.serial, maxBytes: 8 * 1024 * 1024 });
72
+ if (result.exitCode !== 0)
73
+ throw classifyFailure(result);
74
+ const fresh = detectNewCrashes(parseLogcat(result.stdout), seen, withStacks);
75
+ if (fresh.length > 0) {
76
+ return { matched: true, waitedMs: Date.now() - start, crashes: fresh };
77
+ }
78
+ }
79
+ catch (error) {
80
+ lastError = error instanceof Error ? error.message : String(error);
81
+ }
82
+ if (Date.now() >= deadline) {
83
+ return {
84
+ matched: false,
85
+ waitedMs: timeoutMs,
86
+ crashes: [],
87
+ ...(lastError !== undefined ? { reason: lastError } : { reason: `no new crash within ${timeoutMs}ms` }),
88
+ };
89
+ }
90
+ await sleep(Math.min(intervalMs, deadline - Date.now()), signal);
91
+ }
92
+ }
93
+ /** Format detected crashes as a job_output-friendly text block. */
94
+ export function formatCrashDetections(detection) {
95
+ const lines = [`[adb_watch_crash] ${detection.matched ? `detected ${detection.crashes.length} new crash(es) after ${detection.waitedMs}ms` : `no new crash within ${detection.waitedMs}ms`}`];
96
+ for (const crash of detection.crashes) {
97
+ lines.push(`- ${crash.time} pid=${crash.pid} ${crash.tag}: ${crash.message}`);
98
+ for (const line of crash.stack)
99
+ lines.push(` ${line}`);
100
+ }
101
+ return lines.join('\n');
102
+ }
103
+ /** adb_watch_crash: watch the crash buffer for NEW real crashes (foreground poll or background job). */
104
+ export function registerWatchCrashTool(ctx, cfg) {
105
+ ctx.tools.register({
106
+ name: 'adb_watch_crash',
107
+ description: 'Watch the crash buffer for NEW real crashes. Foreground: poll until a new real crash (FATAL EXCEPTION / Fatal signal / SIG*) appears or the budget expires — returns matched:false on timeout (not an error), so you can react. Background (run_in_background): keeps polling as a job; job_output returns a text summary of each newly detected crash. Boot markers (mtk-brm) are not crashes. Pair with adb_crash_report / adb_screenshot to capture the scene when a crash is detected.',
108
+ parameters: {
109
+ type: 'object',
110
+ additionalProperties: false,
111
+ properties: {
112
+ serial: { type: 'string', description: 'Target device serial; defaults to the plugin defaultSerial.' },
113
+ timeoutMs: { type: 'integer', description: 'Overall watch budget in ms; defaults to 60000, capped at 600000.' },
114
+ intervalMs: { type: 'integer', description: 'Poll interval in ms; defaults to 1000, minimum 250.' },
115
+ withStacks: { type: 'boolean', description: 'Include the following same-pid lines (stack) per crash; defaults to true.' },
116
+ run_in_background: { type: 'boolean', description: 'Run as a background job and return a job id immediately.' },
117
+ },
118
+ },
119
+ output: jsonOutput(),
120
+ async execute(args, exec) {
121
+ if (args.run_in_background === true) {
122
+ return startBackgroundWatch(ctx, cfg, args, exec);
123
+ }
124
+ return watchForCrash(ctx, cfg, exec.signal, args);
125
+ },
126
+ });
127
+ }
128
+ function startBackgroundWatch(ctx, cfg, args, exec) {
129
+ const jobs = ctx.get('jobs');
130
+ if (jobs === undefined) {
131
+ throw new AdbError('JOBS_UNAVAILABLE', 'background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs');
132
+ }
133
+ if (exec.signal.aborted) {
134
+ const error = new Error('tool call aborted');
135
+ error.name = 'AbortError';
136
+ throw error;
137
+ }
138
+ // The background loop must outlive the tool call: no exec.signal in the loop.
139
+ const controller = new AbortController();
140
+ let latestDetection;
141
+ let done = false;
142
+ const id = jobs.start({
143
+ kind: 'adb-watch-crash',
144
+ label: `watch-crash${args.serial !== undefined ? ` ${args.serial}` : ''}`,
145
+ ...(exec.agent !== undefined ? { owner: exec.agent } : {}),
146
+ run: () => {
147
+ const loop = (async () => {
148
+ const start = Date.now();
149
+ const timeoutMs = Math.min(Math.floor(args.timeoutMs ?? DEFAULT_TIMEOUT_MS), MAX_TIMEOUT_MS);
150
+ const intervalMs = Math.max(Math.floor(args.intervalMs ?? DEFAULT_INTERVAL_MS), 250);
151
+ const seen = new Set();
152
+ const withStacks = args.withStacks ?? true;
153
+ const deadline = start + timeoutMs;
154
+ let lastError;
155
+ try {
156
+ const seed = await runAdb(ctx, cfg, ['logcat', '-b', 'crash', '-v', 'threadtime', '-d'], { signal: controller.signal, serial: args.serial, maxBytes: 8 * 1024 * 1024 });
157
+ if (seed.exitCode !== 0)
158
+ throw classifyFailure(seed);
159
+ for (const chain of classifyCrashBuffer(parseLogcat(seed.stdout)).chains) {
160
+ seen.add(crashSignature(chain));
161
+ }
162
+ }
163
+ catch (error) {
164
+ lastError = error instanceof Error ? error.message : String(error);
165
+ }
166
+ while (!done && Date.now() < deadline) {
167
+ try {
168
+ const result = await runAdb(ctx, cfg, ['logcat', '-b', 'crash', '-v', 'threadtime', '-d'], { signal: controller.signal, serial: args.serial, maxBytes: 8 * 1024 * 1024 });
169
+ if (result.exitCode !== 0)
170
+ throw classifyFailure(result);
171
+ const fresh = detectNewCrashes(parseLogcat(result.stdout), seen, withStacks);
172
+ if (fresh.length > 0) {
173
+ latestDetection = { matched: true, waitedMs: Date.now() - start, crashes: fresh };
174
+ }
175
+ }
176
+ catch (error) {
177
+ lastError = error instanceof Error ? error.message : String(error);
178
+ }
179
+ await sleep(Math.min(intervalMs, deadline - Date.now()), controller.signal).catch(() => { });
180
+ }
181
+ done = true;
182
+ if (latestDetection === undefined) {
183
+ latestDetection = {
184
+ matched: false,
185
+ waitedMs: timeoutMs,
186
+ crashes: [],
187
+ ...(lastError !== undefined ? { reason: lastError } : { reason: `no new crash within ${timeoutMs}ms` }),
188
+ };
189
+ }
190
+ })();
191
+ return {
192
+ cancel: () => { done = true; },
193
+ done: loop.then(() => ({ exitCode: 0, signal: null })),
194
+ readOutput: () => latestDetection === undefined ? '' : formatCrashDetections(latestDetection),
195
+ };
196
+ },
197
+ });
198
+ return { kind: 'background', jobId: id };
199
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-adb",
3
- "version": "1.5.0",
4
- "description": "ADB device & bench operations for DeepSeek Harness: device discovery, structured logcat, apk install, file pull/push, performance snapshots, perf baselines, crash reports, one-click device health reports, condition waits, operation ledger w/ rollback, screenshots, web device panel (autocomplete, live logcat, profiler)",
3
+ "version": "1.6.1",
4
+ "description": "ADB device & bench operations for DeepSeek Harness: device discovery, structured logcat, apk install, file pull/push, performance snapshots, perf baselines, crash reports, one-click device health reports, condition waits, operation ledger w/ rollback, screenshots, crash watchdog, web device panel (autocomplete, live logcat, profiler)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {