openclaw-skill-claude-code 1.0.0
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/.eslintrc.json +14 -0
- package/.github/workflows/ci.yml +54 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.prettierrc +6 -0
- package/CONTRIBUTING.md +35 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/SKILL.md +109 -0
- package/TASK.md +178 -0
- package/commitlint.config.js +1 -0
- package/package.json +31 -0
- package/scripts/job-manager.mjs +174 -0
- package/scripts/run.mjs +90 -0
- package/scripts/worker.mjs +183 -0
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
with:
|
|
15
|
+
fetch-depth: 0
|
|
16
|
+
|
|
17
|
+
- name: Setup Node.js
|
|
18
|
+
uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: '20'
|
|
21
|
+
cache: 'npm'
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: npm ci
|
|
25
|
+
|
|
26
|
+
- name: Lint
|
|
27
|
+
run: npm run lint
|
|
28
|
+
|
|
29
|
+
- name: Format Check
|
|
30
|
+
run: npm run format:check
|
|
31
|
+
|
|
32
|
+
- name: Commitlint
|
|
33
|
+
if: github.event_name == 'pull_request'
|
|
34
|
+
uses: wagoid/commitlint-github-action@v5
|
|
35
|
+
|
|
36
|
+
release:
|
|
37
|
+
needs: build
|
|
38
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
permissions:
|
|
41
|
+
contents: write
|
|
42
|
+
issues: write
|
|
43
|
+
pull-requests: write
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
- uses: actions/setup-node@v4
|
|
47
|
+
with:
|
|
48
|
+
node-version: '20'
|
|
49
|
+
cache: 'npm'
|
|
50
|
+
- run: npm ci
|
|
51
|
+
- run: npx semantic-release
|
|
52
|
+
env:
|
|
53
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
54
|
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npx --no -- commitlint --edit ${1}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npm test
|
package/.prettierrc
ADDED
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Contributing to openclaw-skill-claude-code
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing! We want to make this skill the gold standard for robust agentic coding.
|
|
4
|
+
|
|
5
|
+
## Development Workflow
|
|
6
|
+
|
|
7
|
+
1. **Fork & Clone**: Fork the repo and clone it locally.
|
|
8
|
+
2. **Install Dependencies**: \`npm install\`
|
|
9
|
+
3. **Branch**: Create a feature branch (\`feat/my-feature\`).
|
|
10
|
+
4. **Code**: Implement your changes.
|
|
11
|
+
5. **Commit**: Use [Conventional Commits](https://www.conventionalcommits.org/).
|
|
12
|
+
* \`feat: add timeout handling\`
|
|
13
|
+
* \`fix: correct pid tracking\`
|
|
14
|
+
* \`docs: update readme\`
|
|
15
|
+
6. **Push & PR**: Push your branch and open a Pull Request.
|
|
16
|
+
|
|
17
|
+
## Commits
|
|
18
|
+
|
|
19
|
+
We enforce Conventional Commits via \`commitlint\`. This enables automated semantic versioning and changelog generation.
|
|
20
|
+
|
|
21
|
+
Types:
|
|
22
|
+
- \`feat\`: A new feature
|
|
23
|
+
- \`fix\`: A bug fix
|
|
24
|
+
- \`docs\`: Documentation only changes
|
|
25
|
+
- \`style\`: Changes that do not affect the meaning of the code (white-space, formatting, etc)
|
|
26
|
+
- \`refactor\`: A code change that neither fixes a bug nor adds a feature
|
|
27
|
+
- \`perf\`: A code change that improves performance
|
|
28
|
+
- \`test\`: Adding missing tests or correcting existing tests
|
|
29
|
+
- \`chore\`: Changes to the build process or auxiliary tools and libraries such as documentation generation
|
|
30
|
+
|
|
31
|
+
## Release Process
|
|
32
|
+
|
|
33
|
+
Releases are automated via GitHub Actions and \`semantic-release\`.
|
|
34
|
+
- Merging to \`main\` triggers a release.
|
|
35
|
+
- Version number is determined by commit types (fix=patch, feat=minor, BREAKING CHANGE=major).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Noncelogic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Resilient Claude Code Skill
|
|
2
|
+
|
|
3
|
+
**Persistent, detached coding jobs for OpenClaw.**
|
|
4
|
+
|
|
5
|
+
Most Claude Code wrappers run `exec('claude ...')`. If your agent turn times out, the gateway restarts, or the API hangs — your coding task dies.
|
|
6
|
+
|
|
7
|
+
This skill treats coding tasks as **persistent, detached jobs** using the `@anthropic-ai/claude-agent-sdk`.
|
|
8
|
+
|
|
9
|
+
## Why This Exists
|
|
10
|
+
|
|
11
|
+
### Decoupled Lifecycle
|
|
12
|
+
Agent turns have timeouts (e.g., 900s). Coding tasks take 20+ minutes. This skill starts a Claude agent process, **detaches it** from the agent session, and returns a `jobId`. The agent checks back later.
|
|
13
|
+
|
|
14
|
+
### Restart Survival
|
|
15
|
+
If you restart OpenClaw (maintenance, crash, update), child processes usually get killed. This skill spawns processes in a separate process group with PID tracking on disk. On reboot, it **re-acquires running jobs** instead of losing them.
|
|
16
|
+
|
|
17
|
+
### Rate Limit Intelligence
|
|
18
|
+
Generic wrappers treat any pause as a hang. This skill distinguishes between "Claude is thinking" and "API 429/503", bubbling precise status to the orchestrator.
|
|
19
|
+
|
|
20
|
+
## Architecture
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
[ OpenClaw Agent ] -> (SKILL.md instructions) -> [ exec: node scripts/run.mjs ]
|
|
24
|
+
|
|
|
25
|
+
(Spawn detached job)
|
|
26
|
+
|
|
|
27
|
+
[ Claude Agent SDK ]
|
|
28
|
+
|
|
|
29
|
+
(Write state to disk)
|
|
30
|
+
v
|
|
31
|
+
[ jobs/<jobId>/ ]
|
|
32
|
+
├── meta.json (status, timestamps, pid)
|
|
33
|
+
├── output.log (streaming output)
|
|
34
|
+
└── result.json (final result when done)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
1. Agent calls `start` — gets a `jobId` and `pid`.
|
|
38
|
+
2. Agent turn ends. The coding job keeps running.
|
|
39
|
+
3. Agent polls `status` on next turn.
|
|
40
|
+
4. Skill reads process status and log tail from disk.
|
|
41
|
+
5. Returns: `running` | `completed` | `failed` | `killed`.
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
### Start a job
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
node scripts/run.mjs start \
|
|
49
|
+
--prompt "Read TASK.md and implement. Run tests." \
|
|
50
|
+
--cwd /path/to/project \
|
|
51
|
+
--job-id task-137
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Check status
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
node scripts/run.mjs status --job-id task-137
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Get logs
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
node scripts/run.mjs logs --job-id task-137 --tail 50
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Get result
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
node scripts/run.mjs result --job-id task-137
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### List all jobs
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
node scripts/run.mjs list
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Kill a job
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
node scripts/run.mjs kill --job-id task-137
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## File Structure
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
openclaw-skill-claude-code/
|
|
88
|
+
├── SKILL.md # OpenClaw skill instructions
|
|
89
|
+
├── README.md # Documentation
|
|
90
|
+
├── package.json # Dependencies
|
|
91
|
+
├── scripts/
|
|
92
|
+
│ ├── run.mjs # CLI entry point
|
|
93
|
+
│ ├── job-manager.mjs # Job lifecycle management
|
|
94
|
+
│ └── worker.mjs # Detached worker process
|
|
95
|
+
└── jobs/ # Runtime state (gitignored)
|
|
96
|
+
└── <jobId>/
|
|
97
|
+
├── meta.json
|
|
98
|
+
├── output.log
|
|
99
|
+
└── result.json
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Configuration
|
|
103
|
+
|
|
104
|
+
| Variable | Required | Default | Description |
|
|
105
|
+
|----------|----------|---------|-------------|
|
|
106
|
+
| `ANTHROPIC_API_KEY` | Yes | — | Claude API key |
|
|
107
|
+
| `CLAUDE_SKILL_JOBS_DIR` | No | `<skill-dir>/jobs` | Directory for job state |
|
|
108
|
+
| `CLAUDE_SKILL_MODEL` | No | SDK default | Override the model |
|
|
109
|
+
|
|
110
|
+
## How It Works
|
|
111
|
+
|
|
112
|
+
**Job Manager** (`scripts/job-manager.mjs`): Manages the job lifecycle — `start`, `status`, `result`, `logs`, `list`, `kill`. Spawns workers as detached processes and tracks them by PID on disk.
|
|
113
|
+
|
|
114
|
+
**Worker** (`scripts/worker.mjs`): Runs in a detached process. Streams the Claude Agent SDK `query()` iterator, writing output to `output.log` and final results to `result.json`. Handles SIGTERM gracefully and detects rate limits.
|
|
115
|
+
|
|
116
|
+
**CLI** (`scripts/run.mjs`): Thin command-line wrapper over the job manager. All output is structured JSON.
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: claude-code
|
|
3
|
+
description: Run coding tasks as persistent, detached jobs using the Claude Agent SDK. Jobs survive agent timeouts, gateway restarts, and API hangs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Claude Code Skill
|
|
7
|
+
|
|
8
|
+
This skill runs coding tasks as **persistent, detached jobs** using the `@anthropic-ai/claude-agent-sdk`. Unlike raw `exec claude` calls, jobs survive agent turn timeouts, gateway restarts, and API rate limits.
|
|
9
|
+
|
|
10
|
+
## Starting a coding task
|
|
11
|
+
|
|
12
|
+
Use `exec` to start a detached job. It returns immediately with a `jobId`.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
node {{skill_dir}}/scripts/run.mjs start \
|
|
16
|
+
--prompt "Read TASK.md and implement. Run tests." \
|
|
17
|
+
--cwd /path/to/project \
|
|
18
|
+
--job-id task-137
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Arguments:**
|
|
22
|
+
- `--prompt` (required): The task description for Claude.
|
|
23
|
+
- `--cwd` (required): The working directory for the coding task.
|
|
24
|
+
- `--job-id` (required): A unique identifier for this job.
|
|
25
|
+
- `--model` (optional): Override the model (defaults to SDK default).
|
|
26
|
+
|
|
27
|
+
**Returns JSON:**
|
|
28
|
+
```json
|
|
29
|
+
{ "jobId": "task-137", "pid": 12345, "status": "running" }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Checking status
|
|
33
|
+
|
|
34
|
+
Poll job status to see if it's still running, completed, or failed.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
node {{skill_dir}}/scripts/run.mjs status --job-id task-137
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Returns JSON:**
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"jobId": "task-137",
|
|
44
|
+
"pid": 12345,
|
|
45
|
+
"status": "running",
|
|
46
|
+
"startedAt": "2025-01-01T00:00:00.000Z",
|
|
47
|
+
"endedAt": null,
|
|
48
|
+
"error": null,
|
|
49
|
+
"rateLimited": false
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Status values:** `running`, `completed`, `failed`, `killed`, `not_found`
|
|
54
|
+
|
|
55
|
+
If `rateLimited` is `true`, the job hit API rate limits — this is a temporary condition, not a hang.
|
|
56
|
+
|
|
57
|
+
## Getting logs
|
|
58
|
+
|
|
59
|
+
Read the last N lines of streaming output from the job.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
node {{skill_dir}}/scripts/run.mjs logs --job-id task-137 --tail 50
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Getting the result
|
|
66
|
+
|
|
67
|
+
Once status is `completed`, retrieve the final result.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
node {{skill_dir}}/scripts/run.mjs result --job-id task-137
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Returns JSON:**
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"jobId": "task-137",
|
|
77
|
+
"status": "completed",
|
|
78
|
+
"result": "...",
|
|
79
|
+
"cost_usd": 0.05,
|
|
80
|
+
"duration_ms": 120000,
|
|
81
|
+
"num_turns": 8
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Listing all jobs
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
node {{skill_dir}}/scripts/run.mjs list
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Killing a job
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
node {{skill_dir}}/scripts/run.mjs kill --job-id task-137
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Best Practices
|
|
98
|
+
|
|
99
|
+
- **Unique job IDs**: Use descriptive IDs like `issue-42` or `feat-auth-v2` to track jobs.
|
|
100
|
+
- **Poll status**: After starting a job, check status periodically. Jobs can take 5-30 minutes.
|
|
101
|
+
- **Check logs on failure**: If status is `failed`, read logs to understand what went wrong.
|
|
102
|
+
- **Rate limits are normal**: If `rateLimited` is true, the SDK is handling retries automatically.
|
|
103
|
+
- **Verification**: Include "Run tests" in your prompt to have Claude verify its own changes.
|
|
104
|
+
|
|
105
|
+
## Environment
|
|
106
|
+
|
|
107
|
+
- `ANTHROPIC_API_KEY` — required (the skill checks for this and fails fast with a clear error if missing).
|
|
108
|
+
- `CLAUDE_SKILL_JOBS_DIR` — optional, defaults to `<skill_dir>/jobs`.
|
|
109
|
+
- `CLAUDE_SKILL_MODEL` — optional, overrides the default model.
|
package/TASK.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# TASK: Build Resilient Claude Code Skill for OpenClaw (#1)
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
This is an OpenClaw skill that wraps the `@anthropic-ai/claude-agent-sdk` (the official Claude Agent SDK) to run coding tasks as persistent, detached jobs. The current method (`exec claude --dangerously-skip-permissions`) is fragile — jobs die on gateway restart, timeouts, or API hangs.
|
|
5
|
+
|
|
6
|
+
## Architecture
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
[ OpenClaw Agent ] -> (SKILL.md instructions) -> [ exec: node scripts/run.mjs ]
|
|
10
|
+
|
|
|
11
|
+
(Spawn detached job)
|
|
12
|
+
|
|
|
13
|
+
[ Claude Agent SDK ]
|
|
14
|
+
|
|
|
15
|
+
(Write state to disk)
|
|
16
|
+
v
|
|
17
|
+
[ jobs/<jobId>/ ]
|
|
18
|
+
├── meta.json (status, timestamps, pid)
|
|
19
|
+
├── output.log (streaming output)
|
|
20
|
+
└── result.json (final result when done)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## SDK Reference
|
|
24
|
+
|
|
25
|
+
The official SDK is `@anthropic-ai/claude-agent-sdk` (v0.2.42). TypeScript usage:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
29
|
+
|
|
30
|
+
for await (const message of query({
|
|
31
|
+
prompt: "Fix the bug in auth.py",
|
|
32
|
+
options: {
|
|
33
|
+
allowedTools: ["Read", "Edit", "Bash", "Glob", "Grep"],
|
|
34
|
+
permissionMode: "acceptEdits", // Auto-approve file edits
|
|
35
|
+
}
|
|
36
|
+
})) {
|
|
37
|
+
if (message.type === "assistant" && message.message?.content) {
|
|
38
|
+
for (const block of message.message.content) {
|
|
39
|
+
if ("text" in block) console.log(block.text);
|
|
40
|
+
else if ("name" in block) console.log(`Tool: ${block.name}`);
|
|
41
|
+
}
|
|
42
|
+
} else if (message.type === "result") {
|
|
43
|
+
console.log(`Done: ${message.subtype}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Auth: Set `ANTHROPIC_API_KEY` env var. The SDK reads it automatically.
|
|
49
|
+
|
|
50
|
+
Available tools: `Read`, `Write`, `Edit`, `Bash`, `Glob`, `Grep`, `WebSearch`, `WebFetch`.
|
|
51
|
+
|
|
52
|
+
## Implementation
|
|
53
|
+
|
|
54
|
+
### 1. Package Setup
|
|
55
|
+
Create `package.json` with dependencies:
|
|
56
|
+
- `@anthropic-ai/claude-agent-sdk`
|
|
57
|
+
- No other runtime deps needed
|
|
58
|
+
|
|
59
|
+
### 2. Job Manager (`scripts/job-manager.mjs`)
|
|
60
|
+
Core module that manages job lifecycle:
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
// start(jobId, prompt, cwd, options) → spawns detached process
|
|
64
|
+
// status(jobId) → reads meta.json, checks if PID alive
|
|
65
|
+
// result(jobId) → reads result.json
|
|
66
|
+
// logs(jobId, tail) → reads last N lines of output.log
|
|
67
|
+
// list() → lists all jobs with status
|
|
68
|
+
// kill(jobId) → sends SIGTERM to PID
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**State directory**: `jobs/<jobId>/`
|
|
72
|
+
- `meta.json`: `{ jobId, pid, status, prompt, cwd, startedAt, endedAt, error }`
|
|
73
|
+
- `output.log`: Streaming text output from the agent
|
|
74
|
+
- `result.json`: Final result text (written on completion)
|
|
75
|
+
|
|
76
|
+
**Detached spawn**: The job manager spawns `node scripts/worker.mjs <jobId>` as a detached child process (`{ detached: true, stdio: 'ignore' }`) and immediately unrefs it. The worker runs independently.
|
|
77
|
+
|
|
78
|
+
**PID tracking**: On startup, `status()` checks if the PID in `meta.json` is still alive (`process.kill(pid, 0)`). If the process died without updating status, mark as `failed`.
|
|
79
|
+
|
|
80
|
+
### 3. Worker (`scripts/worker.mjs`)
|
|
81
|
+
Runs in a detached process. Receives `jobId` as argv:
|
|
82
|
+
|
|
83
|
+
1. Read `jobs/<jobId>/meta.json` for prompt and cwd
|
|
84
|
+
2. `process.chdir(cwd)`
|
|
85
|
+
3. Stream `query()` from the Agent SDK
|
|
86
|
+
4. Write each message to `output.log` (append)
|
|
87
|
+
5. On completion: write `result.json`, update `meta.json` status to `completed`
|
|
88
|
+
6. On error: update `meta.json` status to `failed`, write error details
|
|
89
|
+
7. Handle SIGTERM gracefully
|
|
90
|
+
|
|
91
|
+
**Rate limit detection**: Watch for 429/503 patterns in error messages. Set a `rateLimited` flag in `meta.json` so the orchestrator knows it's not a hang.
|
|
92
|
+
|
|
93
|
+
### 4. CLI Entry Points (`scripts/run.mjs`)
|
|
94
|
+
Simple CLI wrappers for the job manager:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Start a job
|
|
98
|
+
node scripts/run.mjs start --prompt "Fix the bug" --cwd /path/to/project --job-id my-job
|
|
99
|
+
|
|
100
|
+
# Check status
|
|
101
|
+
node scripts/run.mjs status --job-id my-job
|
|
102
|
+
|
|
103
|
+
# Get logs (last 50 lines)
|
|
104
|
+
node scripts/run.mjs logs --job-id my-job --tail 50
|
|
105
|
+
|
|
106
|
+
# Get result
|
|
107
|
+
node scripts/run.mjs result --job-id my-job
|
|
108
|
+
|
|
109
|
+
# List all jobs
|
|
110
|
+
node scripts/run.mjs list
|
|
111
|
+
|
|
112
|
+
# Kill a job
|
|
113
|
+
node scripts/run.mjs kill --job-id my-job
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Return JSON to stdout for structured output.
|
|
117
|
+
|
|
118
|
+
### 5. SKILL.md
|
|
119
|
+
Update the skill instructions to use the new scripts instead of raw `exec claude`:
|
|
120
|
+
|
|
121
|
+
```markdown
|
|
122
|
+
## Starting a coding task
|
|
123
|
+
Use `exec` to run:
|
|
124
|
+
\`\`\`bash
|
|
125
|
+
node /path/to/skill/scripts/run.mjs start \
|
|
126
|
+
--prompt "Read TASK.md and implement. Run tests." \
|
|
127
|
+
--cwd /path/to/project \
|
|
128
|
+
--job-id task-137
|
|
129
|
+
\`\`\`
|
|
130
|
+
|
|
131
|
+
## Checking status
|
|
132
|
+
\`\`\`bash
|
|
133
|
+
node /path/to/skill/scripts/run.mjs status --job-id task-137
|
|
134
|
+
\`\`\`
|
|
135
|
+
|
|
136
|
+
## Getting results
|
|
137
|
+
\`\`\`bash
|
|
138
|
+
node /path/to/skill/scripts/run.mjs result --job-id task-137
|
|
139
|
+
\`\`\`
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 6. README.md
|
|
143
|
+
Update with architecture diagram, usage examples, and configuration.
|
|
144
|
+
|
|
145
|
+
## Configuration
|
|
146
|
+
|
|
147
|
+
The skill reads these env vars:
|
|
148
|
+
- `ANTHROPIC_API_KEY` — required (Claude API key)
|
|
149
|
+
- `CLAUDE_SKILL_JOBS_DIR` — optional, defaults to `<skill-dir>/jobs`
|
|
150
|
+
- `CLAUDE_SKILL_MODEL` — optional, defaults to SDK default
|
|
151
|
+
|
|
152
|
+
## File Structure
|
|
153
|
+
```
|
|
154
|
+
openclaw-skill-claude-code/
|
|
155
|
+
├── SKILL.md # OpenClaw skill instructions
|
|
156
|
+
├── README.md # Documentation
|
|
157
|
+
├── package.json # Dependencies
|
|
158
|
+
├── scripts/
|
|
159
|
+
│ ├── run.mjs # CLI entry point
|
|
160
|
+
│ ├── job-manager.mjs # Job lifecycle management
|
|
161
|
+
│ └── worker.mjs # Detached worker process
|
|
162
|
+
└── jobs/ # Runtime state (gitignored)
|
|
163
|
+
└── <jobId>/
|
|
164
|
+
├── meta.json
|
|
165
|
+
├── output.log
|
|
166
|
+
└── result.json
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Verification
|
|
170
|
+
- `npm install` succeeds
|
|
171
|
+
- `node scripts/run.mjs start --prompt "What files are here?" --cwd . --job-id test-1` starts a job
|
|
172
|
+
- `node scripts/run.mjs status --job-id test-1` shows status
|
|
173
|
+
- `node scripts/run.mjs list` shows all jobs
|
|
174
|
+
- Job survives if parent process exits
|
|
175
|
+
- Handles missing ANTHROPIC_API_KEY gracefully (clear error message)
|
|
176
|
+
|
|
177
|
+
## Branch
|
|
178
|
+
`feat/1-resilient-job-manager`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default { extends: ['@commitlint/config-conventional'] };
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-skill-claude-code",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Resilient Claude Code skill for OpenClaw — persistent, detached coding jobs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node scripts/run.mjs",
|
|
8
|
+
"lint": "eslint .",
|
|
9
|
+
"format": "prettier --write .",
|
|
10
|
+
"format:check": "prettier --check .",
|
|
11
|
+
"prepare": "husky",
|
|
12
|
+
"test": "echo \"No tests yet\" && exit 0"
|
|
13
|
+
},
|
|
14
|
+
"release": {
|
|
15
|
+
"branches": [
|
|
16
|
+
"main"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.42"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@commitlint/cli": "^20.4.1",
|
|
24
|
+
"@commitlint/config-conventional": "^20.4.1",
|
|
25
|
+
"eslint": "^10.0.0",
|
|
26
|
+
"eslint-config-prettier": "^10.1.8",
|
|
27
|
+
"husky": "^9.1.7",
|
|
28
|
+
"prettier": "^3.8.1",
|
|
29
|
+
"semantic-release": "^25.0.3"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const SKILL_DIR = join(__dirname, "..");
|
|
8
|
+
const JOBS_DIR = process.env.CLAUDE_SKILL_JOBS_DIR || join(SKILL_DIR, "jobs");
|
|
9
|
+
|
|
10
|
+
async function ensureJobDir(jobId) {
|
|
11
|
+
const dir = join(JOBS_DIR, jobId);
|
|
12
|
+
await mkdir(dir, { recursive: true });
|
|
13
|
+
return dir;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function metaPath(jobId) {
|
|
17
|
+
return join(JOBS_DIR, jobId, "meta.json");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function outputPath(jobId) {
|
|
21
|
+
return join(JOBS_DIR, jobId, "output.log");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resultPath(jobId) {
|
|
25
|
+
return join(JOBS_DIR, jobId, "result.json");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isPidAlive(pid) {
|
|
29
|
+
try {
|
|
30
|
+
process.kill(pid, 0);
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function readJson(path) {
|
|
38
|
+
const data = await readFile(path, "utf-8");
|
|
39
|
+
return JSON.parse(data);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function writeJson(path, data) {
|
|
43
|
+
await writeFile(path, JSON.stringify(data, null, 2) + "\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function start(jobId, prompt, cwd, options = {}) {
|
|
47
|
+
const dir = await ensureJobDir(jobId);
|
|
48
|
+
|
|
49
|
+
const meta = {
|
|
50
|
+
jobId,
|
|
51
|
+
pid: null,
|
|
52
|
+
status: "starting",
|
|
53
|
+
prompt,
|
|
54
|
+
cwd,
|
|
55
|
+
model: options.model || process.env.CLAUDE_SKILL_MODEL || undefined,
|
|
56
|
+
allowedTools: options.allowedTools || undefined,
|
|
57
|
+
startedAt: new Date().toISOString(),
|
|
58
|
+
endedAt: null,
|
|
59
|
+
error: null,
|
|
60
|
+
rateLimited: false,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
await writeJson(metaPath(jobId), meta);
|
|
64
|
+
await writeFile(outputPath(jobId), "");
|
|
65
|
+
|
|
66
|
+
const workerPath = join(__dirname, "worker.mjs");
|
|
67
|
+
const child = spawn("node", [workerPath, jobId], {
|
|
68
|
+
detached: true,
|
|
69
|
+
stdio: "ignore",
|
|
70
|
+
cwd: SKILL_DIR,
|
|
71
|
+
env: { ...process.env },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
child.unref();
|
|
75
|
+
|
|
76
|
+
meta.pid = child.pid;
|
|
77
|
+
meta.status = "running";
|
|
78
|
+
await writeJson(metaPath(jobId), meta);
|
|
79
|
+
|
|
80
|
+
return { jobId, pid: child.pid, status: "running" };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function status(jobId) {
|
|
84
|
+
let meta;
|
|
85
|
+
try {
|
|
86
|
+
meta = await readJson(metaPath(jobId));
|
|
87
|
+
} catch {
|
|
88
|
+
return { jobId, status: "not_found" };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (meta.status === "running" && meta.pid) {
|
|
92
|
+
if (!isPidAlive(meta.pid)) {
|
|
93
|
+
meta.status = "failed";
|
|
94
|
+
meta.error = "Process exited unexpectedly";
|
|
95
|
+
meta.endedAt = new Date().toISOString();
|
|
96
|
+
await writeJson(metaPath(jobId), meta);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
jobId: meta.jobId,
|
|
102
|
+
pid: meta.pid,
|
|
103
|
+
status: meta.status,
|
|
104
|
+
startedAt: meta.startedAt,
|
|
105
|
+
endedAt: meta.endedAt,
|
|
106
|
+
error: meta.error,
|
|
107
|
+
rateLimited: meta.rateLimited,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function result(jobId) {
|
|
112
|
+
try {
|
|
113
|
+
return await readJson(resultPath(jobId));
|
|
114
|
+
} catch {
|
|
115
|
+
const s = await status(jobId);
|
|
116
|
+
return { jobId, status: s.status, result: null };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function logs(jobId, tail = 50) {
|
|
121
|
+
try {
|
|
122
|
+
const content = await readFile(outputPath(jobId), "utf-8");
|
|
123
|
+
const lines = content.split("\n");
|
|
124
|
+
const sliced = lines.slice(-tail).join("\n");
|
|
125
|
+
return { jobId, lines: lines.length, tail: sliced };
|
|
126
|
+
} catch {
|
|
127
|
+
return { jobId, lines: 0, tail: "" };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function list() {
|
|
132
|
+
let entries;
|
|
133
|
+
try {
|
|
134
|
+
entries = await readdir(JOBS_DIR, { withFileTypes: true });
|
|
135
|
+
} catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const jobs = [];
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
if (entry.isDirectory()) {
|
|
142
|
+
const s = await status(entry.name);
|
|
143
|
+
jobs.push(s);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return jobs;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function kill(jobId) {
|
|
150
|
+
let meta;
|
|
151
|
+
try {
|
|
152
|
+
meta = await readJson(metaPath(jobId));
|
|
153
|
+
} catch {
|
|
154
|
+
return { jobId, killed: false, error: "Job not found" };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!meta.pid) {
|
|
158
|
+
return { jobId, killed: false, error: "No PID recorded" };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!isPidAlive(meta.pid)) {
|
|
162
|
+
return { jobId, killed: false, error: "Process already dead" };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
process.kill(meta.pid, "SIGTERM");
|
|
167
|
+
meta.status = "killed";
|
|
168
|
+
meta.endedAt = new Date().toISOString();
|
|
169
|
+
await writeJson(metaPath(jobId), meta);
|
|
170
|
+
return { jobId, killed: true };
|
|
171
|
+
} catch (err) {
|
|
172
|
+
return { jobId, killed: false, error: err.message };
|
|
173
|
+
}
|
|
174
|
+
}
|
package/scripts/run.mjs
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { start, status, result, logs, list, kill } from "./job-manager.mjs";
|
|
4
|
+
|
|
5
|
+
function parseArgs(args) {
|
|
6
|
+
const parsed = {};
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
if (args[i].startsWith("--")) {
|
|
9
|
+
const key = args[i].slice(2);
|
|
10
|
+
const next = args[i + 1];
|
|
11
|
+
if (next && !next.startsWith("--")) {
|
|
12
|
+
parsed[key] = next;
|
|
13
|
+
i++;
|
|
14
|
+
} else {
|
|
15
|
+
parsed[key] = true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return parsed;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function output(data) {
|
|
23
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function die(message) {
|
|
27
|
+
output({ error: message });
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
32
|
+
const args = parseArgs(rest);
|
|
33
|
+
|
|
34
|
+
switch (command) {
|
|
35
|
+
case "start": {
|
|
36
|
+
const prompt = args.prompt;
|
|
37
|
+
const cwd = args.cwd || process.cwd();
|
|
38
|
+
const jobId = args["job-id"];
|
|
39
|
+
|
|
40
|
+
if (!prompt) die("--prompt is required");
|
|
41
|
+
if (!jobId) die("--job-id is required");
|
|
42
|
+
|
|
43
|
+
const options = {};
|
|
44
|
+
if (args.model) options.model = args.model;
|
|
45
|
+
|
|
46
|
+
const res = await start(jobId, prompt, cwd, options);
|
|
47
|
+
output(res);
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
case "status": {
|
|
52
|
+
const jobId = args["job-id"];
|
|
53
|
+
if (!jobId) die("--job-id is required");
|
|
54
|
+
output(await status(jobId));
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
case "result": {
|
|
59
|
+
const jobId = args["job-id"];
|
|
60
|
+
if (!jobId) die("--job-id is required");
|
|
61
|
+
output(await result(jobId));
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
case "logs": {
|
|
66
|
+
const jobId = args["job-id"];
|
|
67
|
+
if (!jobId) die("--job-id is required");
|
|
68
|
+
const tail = parseInt(args.tail, 10) || 50;
|
|
69
|
+
output(await logs(jobId, tail));
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
case "list": {
|
|
74
|
+
output(await list());
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case "kill": {
|
|
79
|
+
const jobId = args["job-id"];
|
|
80
|
+
if (!jobId) die("--job-id is required");
|
|
81
|
+
output(await kill(jobId));
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
default:
|
|
86
|
+
die(
|
|
87
|
+
`Unknown command: ${command || "(none)"}. ` +
|
|
88
|
+
`Available: start, status, result, logs, list, kill`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { readFile, writeFile, appendFile } from "node:fs/promises";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const SKILL_DIR = join(__dirname, "..");
|
|
7
|
+
const JOBS_DIR = process.env.CLAUDE_SKILL_JOBS_DIR || join(SKILL_DIR, "jobs");
|
|
8
|
+
|
|
9
|
+
const jobId = process.argv[2];
|
|
10
|
+
if (!jobId) {
|
|
11
|
+
console.error("Usage: node worker.mjs <jobId>");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const metaFile = join(JOBS_DIR, jobId, "meta.json");
|
|
16
|
+
const outputFile = join(JOBS_DIR, jobId, "output.log");
|
|
17
|
+
const resultFile = join(JOBS_DIR, jobId, "result.json");
|
|
18
|
+
|
|
19
|
+
async function readMeta() {
|
|
20
|
+
return JSON.parse(await readFile(metaFile, "utf-8"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function writeMeta(meta) {
|
|
24
|
+
await writeFile(metaFile, JSON.stringify(meta, null, 2) + "\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function log(text) {
|
|
28
|
+
await appendFile(outputFile, text + "\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let shuttingDown = false;
|
|
32
|
+
|
|
33
|
+
process.on("SIGTERM", async () => {
|
|
34
|
+
shuttingDown = true;
|
|
35
|
+
await log("[worker] Received SIGTERM, shutting down gracefully...");
|
|
36
|
+
const meta = await readMeta();
|
|
37
|
+
meta.status = "killed";
|
|
38
|
+
meta.endedAt = new Date().toISOString();
|
|
39
|
+
await writeMeta(meta);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
async function run() {
|
|
44
|
+
const meta = await readMeta();
|
|
45
|
+
|
|
46
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
47
|
+
meta.status = "failed";
|
|
48
|
+
meta.error = "ANTHROPIC_API_KEY environment variable is not set";
|
|
49
|
+
meta.endedAt = new Date().toISOString();
|
|
50
|
+
await writeMeta(meta);
|
|
51
|
+
await log("[worker] ERROR: ANTHROPIC_API_KEY not set");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await log(`[worker] Starting job ${jobId}`);
|
|
56
|
+
await log(`[worker] Prompt: ${meta.prompt}`);
|
|
57
|
+
await log(`[worker] CWD: ${meta.cwd}`);
|
|
58
|
+
|
|
59
|
+
let queryFn;
|
|
60
|
+
try {
|
|
61
|
+
const sdk = await import("@anthropic-ai/claude-agent-sdk");
|
|
62
|
+
queryFn = sdk.query;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
meta.status = "failed";
|
|
65
|
+
meta.error = `Failed to load SDK: ${err.message}`;
|
|
66
|
+
meta.endedAt = new Date().toISOString();
|
|
67
|
+
await writeMeta(meta);
|
|
68
|
+
await log(`[worker] ERROR: ${meta.error}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const options = {
|
|
73
|
+
cwd: meta.cwd,
|
|
74
|
+
permissionMode: "bypassPermissions",
|
|
75
|
+
allowDangerouslySkipPermissions: true,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (meta.model) {
|
|
79
|
+
options.model = meta.model;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (meta.allowedTools) {
|
|
83
|
+
options.allowedTools = meta.allowedTools;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let resultText = "";
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const q = queryFn({ prompt: meta.prompt, options });
|
|
90
|
+
|
|
91
|
+
for await (const message of q) {
|
|
92
|
+
if (shuttingDown) break;
|
|
93
|
+
|
|
94
|
+
if (message.type === "assistant" && message.message?.content) {
|
|
95
|
+
for (const block of message.message.content) {
|
|
96
|
+
if ("text" in block) {
|
|
97
|
+
await log(block.text);
|
|
98
|
+
resultText += block.text + "\n";
|
|
99
|
+
} else if ("name" in block) {
|
|
100
|
+
await log(`[tool] ${block.name}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} else if (message.type === "result") {
|
|
104
|
+
if (message.subtype === "success") {
|
|
105
|
+
await log(`[worker] Completed successfully`);
|
|
106
|
+
resultText = message.result || resultText;
|
|
107
|
+
|
|
108
|
+
const resultData = {
|
|
109
|
+
jobId,
|
|
110
|
+
status: "completed",
|
|
111
|
+
result: resultText,
|
|
112
|
+
cost_usd: message.total_cost_usd,
|
|
113
|
+
duration_ms: message.duration_ms,
|
|
114
|
+
num_turns: message.num_turns,
|
|
115
|
+
};
|
|
116
|
+
await writeFile(resultFile, JSON.stringify(resultData, null, 2) + "\n");
|
|
117
|
+
|
|
118
|
+
meta.status = "completed";
|
|
119
|
+
meta.endedAt = new Date().toISOString();
|
|
120
|
+
await writeMeta(meta);
|
|
121
|
+
} else {
|
|
122
|
+
const errorMsg = message.errors?.join("; ") || message.subtype;
|
|
123
|
+
await log(`[worker] Error: ${errorMsg}`);
|
|
124
|
+
|
|
125
|
+
meta.status = "failed";
|
|
126
|
+
meta.error = errorMsg;
|
|
127
|
+
meta.endedAt = new Date().toISOString();
|
|
128
|
+
await writeMeta(meta);
|
|
129
|
+
|
|
130
|
+
const resultData = {
|
|
131
|
+
jobId,
|
|
132
|
+
status: "failed",
|
|
133
|
+
error: errorMsg,
|
|
134
|
+
result: resultText || null,
|
|
135
|
+
cost_usd: message.total_cost_usd,
|
|
136
|
+
duration_ms: message.duration_ms,
|
|
137
|
+
};
|
|
138
|
+
await writeFile(resultFile, JSON.stringify(resultData, null, 2) + "\n");
|
|
139
|
+
}
|
|
140
|
+
} else if (message.type === "assistant" && message.error) {
|
|
141
|
+
const errType = message.error;
|
|
142
|
+
if (errType === "rate_limit") {
|
|
143
|
+
meta.rateLimited = true;
|
|
144
|
+
await writeMeta(meta);
|
|
145
|
+
await log("[worker] Rate limited, SDK will retry...");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const errMsg = err.message || String(err);
|
|
151
|
+
await log(`[worker] Fatal error: ${errMsg}`);
|
|
152
|
+
|
|
153
|
+
const isRateLimit = /429|rate.limit|503|overloaded/i.test(errMsg);
|
|
154
|
+
meta.status = "failed";
|
|
155
|
+
meta.error = errMsg;
|
|
156
|
+
meta.rateLimited = isRateLimit;
|
|
157
|
+
meta.endedAt = new Date().toISOString();
|
|
158
|
+
await writeMeta(meta);
|
|
159
|
+
|
|
160
|
+
const resultData = {
|
|
161
|
+
jobId,
|
|
162
|
+
status: "failed",
|
|
163
|
+
error: errMsg,
|
|
164
|
+
result: resultText || null,
|
|
165
|
+
};
|
|
166
|
+
await writeFile(resultFile, JSON.stringify(resultData, null, 2) + "\n");
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
run().catch(async (err) => {
|
|
172
|
+
try {
|
|
173
|
+
const meta = await readMeta();
|
|
174
|
+
meta.status = "failed";
|
|
175
|
+
meta.error = err.message || String(err);
|
|
176
|
+
meta.endedAt = new Date().toISOString();
|
|
177
|
+
await writeMeta(meta);
|
|
178
|
+
await log(`[worker] Unhandled error: ${meta.error}`);
|
|
179
|
+
} catch {
|
|
180
|
+
// Can't even write meta, just exit
|
|
181
|
+
}
|
|
182
|
+
process.exit(1);
|
|
183
|
+
});
|