Skip to content

Commit 5506548

Browse files
authored
feat: improve logs debugging workflow (#111)
1 parent 2876c98 commit 5506548

12 files changed

Lines changed: 520 additions & 15 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The project is in early development and considered experimental. Pull requests a
1616
- Platforms: iOS (simulator + physical device core automation) and Android (emulator + device).
1717
- Core commands: `open`, `back`, `home`, `app-switcher`, `press`, `long-press`, `focus`, `type`, `fill`, `scroll`, `scrollintoview`, `wait`, `alert`, `screenshot`, `close`, `reinstall`.
1818
- Inspection commands: `snapshot` (accessibility tree), `diff snapshot` (structural baseline diff), `appstate`, `apps`, `devices`.
19-
- App logs: `logs path` returns session log metadata; `logs start` / `logs stop` stream app output; `logs doctor` checks readiness; `logs mark` writes timeline markers.
19+
- App logs: `logs path` returns session log metadata; `logs start` / `logs stop` stream app output; `logs clear` truncates session app logs; `logs clear --restart` resets and restarts stream in one step; `logs doctor` checks readiness; `logs mark` writes timeline markers.
2020
- Device tooling: `adb` (Android), `simctl`/`devicectl` (iOS via Xcode).
2121
- Minimal dependencies; TypeScript executed directly on Node 22+ (no build step).
2222

@@ -145,7 +145,7 @@ agent-device scrollintoview @e42
145145
- `press` (alias: `click`), `focus`, `type`, `fill`, `long-press`, `swipe`, `scroll`, `scrollintoview`, `pinch`, `is`
146146
- `alert`, `wait`, `screenshot`
147147
- `trace start`, `trace stop`
148-
- `logs path`, `logs start`, `logs stop`, `logs doctor`, `logs mark` (session app log file for grep; iOS simulator + iOS device + Android)
148+
- `logs path`, `logs start`, `logs stop`, `logs clear`, `logs clear --restart`, `logs doctor`, `logs mark` (session app log file for grep; iOS simulator + iOS device + Android)
149149
- `settings wifi|airplane|location on|off`
150150
- `settings faceid match|nonmatch|enroll|unenroll` (iOS simulator only)
151151
- `appstate`, `apps`, `devices`, `session list`
@@ -300,9 +300,11 @@ App state:
300300

301301
## Debug
302302

303-
- **App logs (token-efficient):** With an active session, run `logs path` to get path + state metadata (e.g. `~/.agent-device/sessions/<session>/app.log`). Run `logs start` to stream app output to that file; use `logs stop` to stop. Run `logs doctor` for tool/runtime checks and `logs mark "step"` to insert timeline markers. Grep the file when you need to inspect errors (e.g. `grep -n "Error\|Exception" <path>`) instead of pulling full logs into context. Supported on iOS simulator, iOS physical device, and Android.
303+
- **App logs (token-efficient):** Logging is off by default in normal flows. Enable it on demand when debugging. With an active session, run `logs path` to get path + state metadata (e.g. `~/.agent-device/sessions/<session>/app.log`). Run `logs start` to stream app output to that file; use `logs stop` to stop. Run `logs clear` to truncate `app.log` (and remove rotated `app.log.N` files) before a new repro window. Run `logs doctor` for tool/runtime checks and `logs mark "step"` to insert timeline markers. Grep the file when you need to inspect errors (e.g. `grep -n "Error\|Exception" <path>`) instead of pulling full logs into context. Supported on iOS simulator, iOS physical device, and Android.
304+
- Use `logs clear --restart` when you want one command to stop an active stream, clear current logs, and immediately resume streaming.
304305
- `logs start` appends to `app.log` and rotates to `app.log.1` when the file exceeds 5 MB.
305306
- Android log streaming automatically rebinds to the app PID after process restarts.
307+
- Detailed playbook: `skills/agent-device/references/logs-and-debug.md`
306308
- iOS log capture relies on Unified Logging signals (for example `os_log`); plain stdout/stderr output may be limited depending on app/runtime.
307309
- Retention knobs: set `AGENT_DEVICE_APP_LOG_MAX_BYTES` and `AGENT_DEVICE_APP_LOG_MAX_FILES` to override rotation limits.
308310
- Optional write-time redaction patterns: set `AGENT_DEVICE_APP_LOG_REDACT_PATTERNS` to a comma-separated regex list.

skills/agent-device/SKILL.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,18 @@ Think of them as validate/invalidate outcomes when describing intent.
112112
### Logs (token-efficient debugging)
113113

114114
Use the detailed logs workflow reference:
115-
`skills/agent-device/references/logs.md`
115+
`skills/agent-device/references/logs-and-debug.md`
116116

117117
Recommended minimum:
118118

119119
```bash
120120
agent-device logs doctor
121-
agent-device logs start
121+
agent-device logs clear --restart
122122
agent-device logs path
123123
```
124124

125+
Logging is off by default for normal flows. Turn it on only for debugging windows.
126+
125127
### App state
126128

127129
```bash

skills/agent-device/references/logs.md renamed to skills/agent-device/references/logs-and-debug.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Logs (Token-Efficient Debugging)
22

3-
App output is written to a session-scoped file so agents can grep it instead of loading full logs into context.
3+
Logging is off by default in normal flows. Enable it on demand for debugging windows. App output is written to a session-scoped file so agents can grep it instead of loading full logs into context.
44

55
## Quick Flow
66

77
```bash
88
agent-device open MyApp --platform ios
9-
agent-device logs start # Start streaming app logs to session file
9+
agent-device logs clear --restart # Preferred: stop stream, clear logs, and start streaming again
1010
agent-device logs path # Print path, e.g. ~/.agent-device/sessions/default/app.log
1111
agent-device logs doctor # Check tool/runtime readiness for current session/device
1212
agent-device logs mark "before tap" # Insert a timeline marker into app.log
@@ -19,6 +19,8 @@ agent-device logs stop # Stop streaming (optional; close also stop
1919
- `logs path`: returns log file path and metadata (`active`, `state`, `backend`, size, timestamps).
2020
- `logs start`: starts streaming; requires an active app session (`open` first). Supported on iOS simulator, iOS device, and Android.
2121
- `logs stop`: stops streaming. Session `close` also stops logging.
22+
- `logs clear`: truncates `app.log` and removes rotated `app.log.N` files. Requires logging to be stopped first.
23+
- `logs clear --restart`: convenience reset for repro loops (stop stream, clear files, restart stream).
2224
- `logs doctor`: reports backend/tool checks and readiness notes for troubleshooting.
2325
- `logs mark`: writes a timestamped marker line to the session log.
2426

@@ -49,3 +51,35 @@ tail -50 <path>
4951
- Use `-n` for line numbers.
5052
- Use `-E` for extended regex so `|` in the pattern does not need escaping.
5153
- Prefer targeted patterns (e.g. `Error`, `Exception`, or app-specific tags) over reading the full file.
54+
55+
## Crash Triage Fast Path
56+
57+
Always start from the session app log, then branch by platform.
58+
59+
```bash
60+
agent-device logs path
61+
grep -n -E "SIGABRT|SIGSEGV|EXC_|fatal|exception|terminated|killed|jetsam|memorystatus|FATAL EXCEPTION|Abort message" <path>
62+
nl -ba <path> | sed -n '<start>,<end>p'
63+
```
64+
65+
### iOS
66+
67+
```bash
68+
# If log shows ReportCrash / SIGABRT / EXC_*, inspect simulator DiagnosticReports:
69+
ls -lt ~/Library/Logs/DiagnosticReports | grep -E "<AppName>|<BundleId>" | head
70+
```
71+
72+
- `SIGABRT`: app/runtime abort; inspect `.ips` triggered thread and top frames.
73+
- `SIGKILL` + jetsam/memorystatus markers: memory-pressure kill.
74+
- `EXC_BAD_ACCESS`/`SIGSEGV`: native memory access issue.
75+
76+
### Android
77+
78+
```bash
79+
# Capture fatal crash lines around app process death:
80+
adb -s <serial> logcat -d | grep -n -E "FATAL EXCEPTION|Process: <package>|Abort message|signal [0-9]+ \\(SIG"
81+
```
82+
83+
- `FATAL EXCEPTION` with Java stack: uncaught Java/Kotlin exception.
84+
- `signal 6 (SIGABRT)` or `signal 11 (SIGSEGV)` with tombstone refs: native crash path (NDK/JNI/runtime).
85+
- `Low memory killer` / `Killing <pid>` entries: OS memory-pressure/process reclaim.

src/__tests__/cli-logs.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import { runCli } from '../cli.ts';
4+
import type { DaemonRequest, DaemonResponse } from '../daemon-client.ts';
5+
6+
class ExitSignal extends Error {
7+
public readonly code: number;
8+
9+
constructor(code: number) {
10+
super(`EXIT_${code}`);
11+
this.code = code;
12+
}
13+
}
14+
15+
type RunResult = {
16+
code: number | null;
17+
stdout: string;
18+
stderr: string;
19+
calls: Omit<DaemonRequest, 'token'>[];
20+
};
21+
22+
async function runCliCapture(
23+
argv: string[],
24+
responder: (req: Omit<DaemonRequest, 'token'>) => Promise<DaemonResponse>,
25+
): Promise<RunResult> {
26+
let stdout = '';
27+
let stderr = '';
28+
let code: number | null = null;
29+
const calls: Array<Omit<DaemonRequest, 'token'>> = [];
30+
31+
const originalExit = process.exit;
32+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
33+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
34+
35+
(process as any).exit = ((nextCode?: number) => {
36+
throw new ExitSignal(nextCode ?? 0);
37+
}) as typeof process.exit;
38+
(process.stdout as any).write = ((chunk: unknown) => {
39+
stdout += String(chunk);
40+
return true;
41+
}) as typeof process.stdout.write;
42+
(process.stderr as any).write = ((chunk: unknown) => {
43+
stderr += String(chunk);
44+
return true;
45+
}) as typeof process.stderr.write;
46+
47+
const sendToDaemon = async (req: Omit<DaemonRequest, 'token'>): Promise<DaemonResponse> => {
48+
calls.push(req);
49+
return await responder(req);
50+
};
51+
52+
try {
53+
await runCli(argv, { sendToDaemon });
54+
} catch (error) {
55+
if (error instanceof ExitSignal) code = error.code;
56+
else throw error;
57+
} finally {
58+
process.exit = originalExit;
59+
process.stdout.write = originalStdoutWrite;
60+
process.stderr.write = originalStderrWrite;
61+
}
62+
63+
return { code, stdout, stderr, calls };
64+
}
65+
66+
test('logs clear prints action metadata and forwards --restart flag', async () => {
67+
const result = await runCliCapture(['logs', 'clear', '--restart'], async () => ({
68+
ok: true,
69+
data: {
70+
path: '/tmp/app.log',
71+
cleared: true,
72+
restarted: true,
73+
removedRotatedFiles: 2,
74+
},
75+
}));
76+
77+
assert.equal(result.code, null);
78+
assert.equal(result.calls.length, 1);
79+
assert.equal(result.calls[0]?.flags?.restart, true);
80+
assert.match(result.stdout, /\/tmp\/app\.log/);
81+
assert.match(result.stderr, /cleared=true/);
82+
assert.match(result.stderr, /restarted=true/);
83+
assert.match(result.stderr, /removedRotatedFiles=2/);
84+
});

src/cli.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,13 @@ export async function runCli(argv: string[], deps: CliDeps = DEFAULT_CLI_DEPS):
249249
const state = typeof data?.state === 'string' ? data.state : undefined;
250250
const backend = typeof data?.backend === 'string' ? data.backend : undefined;
251251
const sizeBytes = typeof data?.sizeBytes === 'number' ? data.sizeBytes : undefined;
252+
const started = data?.started === true;
253+
const stopped = data?.stopped === true;
254+
const marked = data?.marked === true;
255+
const cleared = data?.cleared === true;
256+
const restarted = data?.restarted === true;
257+
const removedRotatedFiles =
258+
typeof data?.removedRotatedFiles === 'number' ? data.removedRotatedFiles : undefined;
252259
if (!flags.json && (active !== undefined || state || backend || sizeBytes !== undefined)) {
253260
const meta = [
254261
active !== undefined ? `active=${active}` : '',
@@ -258,6 +265,17 @@ export async function runCli(argv: string[], deps: CliDeps = DEFAULT_CLI_DEPS):
258265
].filter(Boolean).join(' ');
259266
if (meta) process.stderr.write(`${meta}\n`);
260267
}
268+
if (!flags.json && (started || stopped || marked || cleared || restarted || removedRotatedFiles !== undefined)) {
269+
const actionMeta = [
270+
started ? 'started=true' : '',
271+
stopped ? 'stopped=true' : '',
272+
marked ? 'marked=true' : '',
273+
cleared ? 'cleared=true' : '',
274+
restarted ? 'restarted=true' : '',
275+
removedRotatedFiles !== undefined ? `removedRotatedFiles=${removedRotatedFiles}` : '',
276+
].filter(Boolean).join(' ');
277+
if (actionMeta) process.stderr.write(`${actionMeta}\n`);
278+
}
261279
if (data?.hint && !flags.json) {
262280
process.stderr.write(`${data.hint}\n`);
263281
}

src/daemon/__tests__/app-log.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
assertAndroidPackageArgSafe,
1010
buildIosDeviceLogStreamArgs,
1111
buildIosLogPredicate,
12+
clearAppLogFiles,
1213
cleanupStaleAppLogProcesses,
1314
getAppLogPathMetadata,
1415
runAppLogDoctor,
@@ -111,6 +112,23 @@ test('appendAppLogMarker writes marker lines and metadata reflects file', () =>
111112
assert.ok(metadata.sizeBytes > 0);
112113
});
113114

115+
test('clearAppLogFiles truncates current log and removes rotated log files', () => {
116+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-app-log-clear-'));
117+
const outPath = path.join(root, 'app.log');
118+
fs.writeFileSync(outPath, 'line1\nline2\n');
119+
fs.writeFileSync(`${outPath}.1`, 'older');
120+
fs.writeFileSync(`${outPath}.2`, 'oldest');
121+
122+
const result = clearAppLogFiles(outPath);
123+
124+
assert.equal(result.path, outPath);
125+
assert.equal(result.cleared, true);
126+
assert.equal(result.removedRotatedFiles, 2);
127+
assert.equal(fs.readFileSync(outPath, 'utf8'), '');
128+
assert.equal(fs.existsSync(`${outPath}.1`), false);
129+
assert.equal(fs.existsSync(`${outPath}.2`), false);
130+
});
131+
114132
test('runAppLogDoctor returns note when app bundle is missing', async () => {
115133
const result = await runAppLogDoctor({
116134
platform: 'android',

src/daemon/app-log.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,4 +496,28 @@ export function appendAppLogMarker(outPath: string, marker: string): void {
496496
fs.appendFileSync(outPath, line, 'utf8');
497497
}
498498

499+
export function clearAppLogFiles(outPath: string): { path: string; cleared: boolean; removedRotatedFiles: number } {
500+
const dir = path.dirname(outPath);
501+
const base = path.basename(outPath);
502+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
503+
if (fs.existsSync(outPath)) {
504+
fs.truncateSync(outPath, 0);
505+
} else {
506+
fs.writeFileSync(outPath, '', 'utf8');
507+
}
508+
let removedRotatedFiles = 0;
509+
for (const entry of fs.readdirSync(dir)) {
510+
if (!entry.startsWith(`${base}.`)) continue;
511+
const suffix = entry.slice(base.length + 1);
512+
if (!/^\d+$/.test(suffix)) continue;
513+
try {
514+
fs.unlinkSync(path.join(dir, entry));
515+
removedRotatedFiles += 1;
516+
} catch {
517+
// best-effort cleanup
518+
}
519+
}
520+
return { path: outPath, cleared: true, removedRotatedFiles };
521+
}
522+
499523
export const APP_LOG_PID_FILENAME = APP_LOG_PID_FILE;

0 commit comments

Comments
 (0)