@songsid/agend 2.1.0-beta.4 → 2.1.0-beta.40
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 +1 -0
- package/README.zh-TW.md +2 -1
- package/dist/access-path.js +3 -3
- package/dist/access-path.js.map +1 -1
- package/dist/agent-cli.js +1 -1
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +8 -0
- package/dist/agent-endpoint.js +36 -8
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/backend/antigravity.d.ts +2 -0
- package/dist/backend/antigravity.js +30 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +2 -0
- package/dist/backend/claude-code.js +16 -4
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/factory.js +4 -1
- package/dist/backend/factory.js.map +1 -1
- package/dist/backend/grok.d.ts +53 -0
- package/dist/backend/grok.js +302 -0
- package/dist/backend/grok.js.map +1 -0
- package/dist/backend/kiro.d.ts +2 -0
- package/dist/backend/kiro.js +32 -3
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +36 -3
- package/dist/backend/types.js +1 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +20 -0
- package/dist/channel/adapters/discord.js +89 -11
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.js +4 -1
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +18 -2
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/chat-export.js +5 -2
- package/dist/chat-export.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +16 -1
- package/dist/classic-channel-manager.js +76 -4
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +116 -19
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +70 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +3 -1
- package/dist/config.js +14 -1
- package/dist/config.js.map +1 -1
- package/dist/context-guardian.d.ts +2 -0
- package/dist/context-guardian.js +10 -2
- package/dist/context-guardian.js.map +1 -1
- package/dist/daemon-entry.js +2 -1
- package/dist/daemon-entry.js.map +1 -1
- package/dist/daemon.d.ts +91 -6
- package/dist/daemon.js +602 -186
- package/dist/daemon.js.map +1 -1
- package/dist/daily-summary.js +7 -2
- package/dist/daily-summary.js.map +1 -1
- package/dist/fleet-context.d.ts +6 -0
- package/dist/fleet-manager.d.ts +113 -8
- package/dist/fleet-manager.js +972 -142
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-system-prompt.js +1 -1
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +19 -13
- package/dist/general-knowledge/skills/fleet-health/SKILL.md +30 -51
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +19 -13
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +1 -5
- package/dist/general-knowledge/skills/scheduling/SKILL.md +41 -0
- package/dist/general-knowledge/skills/session-management/SKILL.md +29 -116
- package/dist/instance-lifecycle.d.ts +15 -1
- package/dist/instance-lifecycle.js +71 -22
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +28 -2
- package/dist/locale.js.map +1 -1
- package/dist/logger.d.ts +6 -0
- package/dist/logger.js +28 -9
- package/dist/logger.js.map +1 -1
- package/dist/outbound-handlers.d.ts +3 -0
- package/dist/outbound-handlers.js +224 -11
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +44 -2
- package/dist/outbound-schemas.js +50 -3
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/pause-marker.d.ts +5 -0
- package/dist/pause-marker.js +36 -0
- package/dist/pause-marker.js.map +1 -0
- package/dist/scheduler/db.d.ts +8 -0
- package/dist/scheduler/db.js +74 -5
- package/dist/scheduler/db.js.map +1 -1
- package/dist/scheduler/db.test.js +50 -0
- package/dist/scheduler/db.test.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +6 -0
- package/dist/scheduler/scheduler.js +99 -16
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/scheduler/scheduler.test.js +131 -1
- package/dist/scheduler/scheduler.test.js.map +1 -1
- package/dist/scheduler/types.d.ts +9 -3
- package/dist/scheduler/types.js.map +1 -1
- package/dist/settings-api.d.ts +20 -2
- package/dist/settings-api.js +213 -9
- package/dist/settings-api.js.map +1 -1
- package/dist/setup-wizard.js +8 -0
- package/dist/setup-wizard.js.map +1 -1
- package/dist/statusline-watcher.d.ts +1 -1
- package/dist/statusline-watcher.js +3 -2
- package/dist/statusline-watcher.js.map +1 -1
- package/dist/tmux-control.d.ts +7 -0
- package/dist/tmux-control.js +22 -1
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.d.ts +1 -1
- package/dist/tmux-manager.js.map +1 -1
- package/dist/topic-commands.d.ts +19 -1
- package/dist/topic-commands.js +148 -23
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +2 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/types.d.ts +29 -1
- package/dist/tz-utils.d.ts +26 -0
- package/dist/tz-utils.js +46 -0
- package/dist/tz-utils.js.map +1 -0
- package/dist/ui/dashboard.html +3 -3
- package/dist/ui/settings.html +453 -74
- package/dist/web-api.d.ts +1 -0
- package/dist/web-api.js +24 -15
- package/dist/web-api.js.map +1 -1
- package/dist/workflow-templates/default.md +0 -4
- package/package.json +2 -1
|
@@ -43,7 +43,7 @@ User messages arrive as text in your prompt with a prefix:
|
|
|
43
43
|
### Scheduling
|
|
44
44
|
| Tool | Purpose |
|
|
45
45
|
|------|---------|
|
|
46
|
-
| \`create_schedule\` | Create a cron-
|
|
46
|
+
| \`create_schedule\` | Create a recurring cron or one-shot scheduled task |
|
|
47
47
|
| \`list_schedules\` | List all schedules |
|
|
48
48
|
| \`update_schedule\` | Update a schedule |
|
|
49
49
|
| \`delete_schedule\` | Delete a schedule |
|
|
@@ -30,29 +30,35 @@ templates: # Reusable fleet deployment templates
|
|
|
30
30
|
- Instance logs: `~/.agend/instances/<name>/output.log`
|
|
31
31
|
- Fleet log: `~/.agend/fleet.log`
|
|
32
32
|
|
|
33
|
+
## kiro-cli UI mode (kiro_ui)
|
|
34
|
+
|
|
35
|
+
For `backend: kiro-cli` instances, `kiro_ui` selects the CLI's UI/agent profile:
|
|
36
|
+
```yaml
|
|
37
|
+
instances:
|
|
38
|
+
my-kiro:
|
|
39
|
+
backend: kiro-cli
|
|
40
|
+
kiro_ui: legacy # default — current stable behavior
|
|
41
|
+
# kiro_ui: tui # try the newer TUI
|
|
42
|
+
# kiro_ui: v3 # try the v3 profile
|
|
43
|
+
```
|
|
44
|
+
Leave unset (or `legacy`) unless testing a newer profile. Ignored by other backends.
|
|
45
|
+
|
|
33
46
|
## Config Validation
|
|
34
47
|
|
|
35
|
-
**
|
|
48
|
+
**After editing fleet.yaml or classicBot.yaml, validate before reloading:**
|
|
49
|
+
- `validate_config` MCP tool (preferred) — checks channels, instances, backends,
|
|
50
|
+
access, and both yaml files. Reports errors **and** warnings.
|
|
51
|
+
- CLI equivalent: `agend validate`
|
|
36
52
|
|
|
37
|
-
|
|
38
|
-
# Validate fleet.yaml syntax
|
|
39
|
-
agend fleet start --dry-run 2>&1 | head -5
|
|
40
|
-
# Or simply:
|
|
41
|
-
node -e "const yaml = require('js-yaml'); const fs = require('fs'); yaml.load(fs.readFileSync('$HOME/.agend/fleet.yaml', 'utf-8')); console.log('✓ valid YAML')"
|
|
42
|
-
```
|
|
53
|
+
Fix all errors before `agend reload`; warnings are advisory.
|
|
43
54
|
|
|
44
55
|
**Common fleet.yaml mistakes:**
|
|
45
56
|
- Missing `channel.mode` field → error on start
|
|
46
57
|
- Wrong indentation (YAML is indent-sensitive)
|
|
47
58
|
- `topic_id` as string vs number (both work, but be consistent)
|
|
48
|
-
- `backend` typo (valid: `claude-code`, `gemini-cli`, `codex`, `opencode`, `kiro-cli`, `antigravity`)
|
|
59
|
+
- `backend` typo (valid: `claude-code`, `gemini-cli`, `codex`, `opencode`, `kiro-cli`, `antigravity`, `grok`)
|
|
49
60
|
- `model` using wrong format for the backend
|
|
50
61
|
|
|
51
|
-
**classicBot.yaml validation:**
|
|
52
|
-
```bash
|
|
53
|
-
node -e "const yaml = require('js-yaml'); const fs = require('fs'); yaml.load(fs.readFileSync('$HOME/.agend/classicBot.yaml', 'utf-8')); console.log('✓ valid YAML')"
|
|
54
|
-
```
|
|
55
|
-
|
|
56
62
|
**Common classicBot.yaml mistakes:**
|
|
57
63
|
- `allowed_guilds` values must be strings (Discord IDs are too large for YAML integers)
|
|
58
64
|
- Channel IDs as keys must be quoted strings
|
|
@@ -1,55 +1,34 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fleet-health
|
|
3
|
-
description: Check instance health
|
|
3
|
+
description: Check instance health and what an agent is doing; recover a stuck instance
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
##
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
If an instance is stuck (busy for >10 minutes with no output), restart it:
|
|
37
|
-
- `restart_instance("<instance-name>")`
|
|
38
|
-
|
|
39
|
-
## Unsticking a Frozen Instance via tmux
|
|
40
|
-
|
|
41
|
-
If an instance is frozen (not responding, no output, no prompt):
|
|
42
|
-
1. Send Ctrl+C via tmux to interrupt the current operation:
|
|
43
|
-
```bash
|
|
44
|
-
tmux send-keys -t agend:<instance-name> C-c
|
|
45
|
-
```
|
|
46
|
-
2. Wait a few seconds, then check if it returned to idle:
|
|
47
|
-
```bash
|
|
48
|
-
tmux capture-pane -t agend:<instance-name> -p | tail -5
|
|
49
|
-
```
|
|
50
|
-
3. If it shows the prompt (`X% !>` or `(To exit the CLI...)`) — it's unstuck. Resend the task.
|
|
51
|
-
4. If still frozen after Ctrl+C, use `restart_instance("<instance-name>")`
|
|
52
|
-
|
|
53
|
-
**When to use Ctrl+C vs restart:**
|
|
54
|
-
- Ctrl+C: instance is alive but stuck on a long operation (API timeout, large file read, infinite loop)
|
|
55
|
-
- restart: instance is completely dead (no tmux pane, crash loop, or Ctrl+C doesn't help)
|
|
6
|
+
## Check health
|
|
7
|
+
|
|
8
|
+
Prefer the fleet tools — they already know each instance's state:
|
|
9
|
+
- `get_fleet_status` — status + execution state (idle / working / stuck) for every instance.
|
|
10
|
+
- `describe_instance("<name>")` — one instance's status, last activity, description.
|
|
11
|
+
- `list_instances` — quick roster.
|
|
12
|
+
|
|
13
|
+
The daemon derives idle/working/stuck itself (per-backend), so you do **not** need to
|
|
14
|
+
scrape prompts. Don't hand-write pane-text checks like `X% !>` — that prompt is
|
|
15
|
+
kiro-specific and wrong for claude-code / codex / grok / antigravity.
|
|
16
|
+
|
|
17
|
+
## See what an agent is actually doing
|
|
18
|
+
|
|
19
|
+
When the user wants the live screen (not just a status word):
|
|
20
|
+
- `get_instance_logs("<name>")` — recent output.
|
|
21
|
+
- Raw terminal (last resort, no tool for the live pane):
|
|
22
|
+
`tmux capture-pane -t agend:<name> -p | tail -20`
|
|
23
|
+
|
|
24
|
+
## Recover a stuck instance
|
|
25
|
+
|
|
26
|
+
Do this when the user asks, or confirm first — don't silently restart others' work.
|
|
27
|
+
- `restart_instance("<name>")` — reloads config, keeps the session. First choice for a
|
|
28
|
+
dead/looping instance.
|
|
29
|
+
- `replace_instance("<name>")` — fresh instance with handover context, when the session
|
|
30
|
+
itself is the problem (see instance-lifecycle skill).
|
|
31
|
+
|
|
32
|
+
A genuinely wedged CLI can sometimes be freed without a restart by interrupting it —
|
|
33
|
+
that is the cancel key, exposed to users as the Cancel button / `/cancel`. Only drop to
|
|
34
|
+
`tmux send-keys -t agend:<name> C-c` (kiro) / `Escape` (others) if the tools aren't enough.
|
|
@@ -26,7 +26,7 @@ description: Fleet restart types, recovery from tmux crash, rate limit handling,
|
|
|
26
26
|
**Update AgEnD to latest version:**
|
|
27
27
|
```bash
|
|
28
28
|
agend update # update to latest
|
|
29
|
-
agend update --version
|
|
29
|
+
agend update --version 2.1.0 # pin a specific version
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
The `agend update` command automatically:
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: instance-lifecycle
|
|
3
|
-
description:
|
|
3
|
+
description: restart vs replace vs pause/wake; when to use each
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## Restart vs Replace
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
- `
|
|
10
|
-
|
|
8
|
+
- `restart_instance("<name>")` — keeps the session, reloads config. Use when config changed.
|
|
9
|
+
- `replace_instance("<name>")` — kills old, creates a fresh instance with handover context.
|
|
10
|
+
Use when the session is the problem: hallucinating / referencing stale info, stuck in a
|
|
11
|
+
tool-call loop, or context is degrading (only backends that report context % surface that).
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
- Instance keeps hallucinating or referencing stale information
|
|
14
|
-
- Instance is stuck in a tool-call loop
|
|
15
|
-
- Context is reported >80% full and responses are degrading (only applicable to backends that report context usage)
|
|
13
|
+
## Pause / Wake (resource management)
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
- `
|
|
19
|
-
-
|
|
20
|
-
-
|
|
15
|
+
- `pause_instance("<name>")` — stop the resident CLI but keep the instance; frees resources.
|
|
16
|
+
- `wake_instance("<name>")` — bring a paused instance back.
|
|
17
|
+
- Instances also pause automatically: `auto_pause_after` (idle minutes) and `warm_cap`
|
|
18
|
+
(fleet-wide cap — the least-recently-active idle instance is paused when over the cap).
|
|
19
|
+
The **general** instance is never auto-paused.
|
|
20
|
+
- You don't need to wake before sending work: delivery wakes a paused instance first.
|
|
21
|
+
|
|
22
|
+
## Monitoring state
|
|
23
|
+
|
|
24
|
+
- `get_fleet_status` / `describe_instance("<name>")` — status + idle/working/stuck + last
|
|
25
|
+
activity (the daemon derives the state; don't scrape pane prompts).
|
|
26
|
+
- For the raw screen, see the fleet-health skill (`get_instance_logs` / tmux capture).
|
|
@@ -14,11 +14,7 @@ Models are specified in fleet.yaml `defaults.model` or per-instance `model` fiel
|
|
|
14
14
|
| **antigravity** | Run `agy models` to see available models | Gemini 3.5 Flash (Medium) |
|
|
15
15
|
| **codex** | `gpt-4o`, `o3`, `o4-mini` | gpt-4o |
|
|
16
16
|
| **opencode** | `opencode models` | depends on provider |
|
|
17
|
-
|
|
18
|
-
**To discover available models for a backend, run the CLI's model listing command:**
|
|
19
|
-
- `agy models` — lists all available models for antigravity
|
|
20
|
-
- `opencode models` — lists all available models for opencode
|
|
21
|
-
- `codex` — check config.toml
|
|
17
|
+
| **grok** | `grok-4.5`, `grok-4.3`, `grok-code`, `grok-build-0.1` | grok default |
|
|
22
18
|
|
|
23
19
|
**Important for antigravity (agy):**
|
|
24
20
|
- `agy models` shows names like `Gemini 3.5 Flash (Medium)` — the parenthetical suffix (Medium/High/Low/Thinking) is the **effort level**, NOT part of the model name.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scheduling
|
|
3
|
+
description: Cron, one-shot, and silent schedules via the schedule MCP tools
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Tools
|
|
7
|
+
|
|
8
|
+
- `create_schedule` — make a schedule (recurring or one-shot).
|
|
9
|
+
- `list_schedules` — list all (optionally filter by target instance).
|
|
10
|
+
- `update_schedule` — change cron/at/message/timezone/enabled of an existing one.
|
|
11
|
+
- `delete_schedule` — remove one.
|
|
12
|
+
|
|
13
|
+
A schedule injects `message` into the `target` instance when it fires
|
|
14
|
+
(`target` defaults to the current instance).
|
|
15
|
+
|
|
16
|
+
## Three ways to schedule
|
|
17
|
+
|
|
18
|
+
**1. Recurring (cron)** — `cron` + optional `timezone` (IANA, default `Asia/Taipei`):
|
|
19
|
+
```
|
|
20
|
+
create_schedule({ cron: "0 9 * * *", message: "Daily standup summary", timezone: "Asia/Taipei" })
|
|
21
|
+
```
|
|
22
|
+
Cron is 5-field (min hour dom mon dow). `0 9 * * *` = 09:00 daily in the given tz.
|
|
23
|
+
|
|
24
|
+
**2. One-shot (at)** — `at` = ISO-8601 datetime **with offset**; runs once then auto-deletes:
|
|
25
|
+
```
|
|
26
|
+
create_schedule({ at: "2026-07-26T14:00:00+08:00", message: "Ship the release", target: "dev1" })
|
|
27
|
+
```
|
|
28
|
+
Exactly one of `cron` / `at` is required (mutually exclusive).
|
|
29
|
+
|
|
30
|
+
**3. Silent** — add `silent: true` to any of the above. The message is pasted straight
|
|
31
|
+
into the target's tmux pane instead of posting to the channel — use for heartbeats /
|
|
32
|
+
background nudges the user shouldn't see:
|
|
33
|
+
```
|
|
34
|
+
create_schedule({ cron: "*/30 * * * *", message: "check CI and self-report if red", silent: true })
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Notes
|
|
38
|
+
|
|
39
|
+
- `timezone` is per-schedule; the fire time is fixed to that zone (DST-safe).
|
|
40
|
+
- Give recurring schedules a `label` so `list_schedules` / `delete_schedule` are easy.
|
|
41
|
+
- One-shot (`at`) needs no cleanup — it deletes itself after firing.
|
|
@@ -1,133 +1,46 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: session-management
|
|
3
|
-
description:
|
|
3
|
+
description: Where kiro-cli and claude-code store sessions, and how to fork one to a new instance
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## Where sessions live
|
|
7
7
|
|
|
8
|
-
kiro-cli
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
**
|
|
14
|
-
- Manual backup: `cp ~/.kiro/sessions/cli/*.json ~/backup/`
|
|
15
|
-
- Finding a specific session: `ls -lt ~/.kiro/sessions/cli/ | head -5`
|
|
16
|
-
- Loading a session into a new instance via `pre_task_command: "/chat load <path>"`
|
|
17
|
-
|
|
18
|
-
## Reviewer Session Management
|
|
19
|
-
|
|
20
|
-
For reviewer instances using kiro-cli:
|
|
21
|
-
- Recommend setting `pre_task_command: "/chat load reviewer-base.json"` in fleet.yaml
|
|
22
|
-
- This loads a base session with review guidelines on every restart
|
|
23
|
-
- Help user create the base session:
|
|
24
|
-
1. Attach to reviewer: `agend attach <reviewer-instance>`
|
|
25
|
-
2. Set up review context and guidelines
|
|
26
|
-
3. Save: `/chat save reviewer-base.json -f`
|
|
27
|
-
4. Add to fleet.yaml under the reviewer instance config:
|
|
28
|
-
```yaml
|
|
29
|
-
instances:
|
|
30
|
-
reviewer-xxx:
|
|
31
|
-
pre_task_command: "/chat load reviewer-base.json"
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Fork Instance (Session Cloning)
|
|
35
|
-
|
|
36
|
-
When user wants to fork/clone an instance's session to a new instance:
|
|
37
|
-
|
|
38
|
-
Steps:
|
|
39
|
-
1. Wait for source instance to be idle (check with tmux capture-pane, look for "X% !>" prompt)
|
|
40
|
-
|
|
41
|
-
2. Save current session on source instance via tmux:
|
|
42
|
-
- `execute_bash`: `tmux send-keys -t agend:<source-instance> '/chat save YYYYMMDD.json -f' Enter`
|
|
43
|
-
- Wait a few seconds for save to complete
|
|
44
|
-
|
|
45
|
-
3. Create new instance:
|
|
46
|
-
- `create_instance` with same backend and working_directory (or new one)
|
|
47
|
-
|
|
48
|
-
4. Copy session file to new instance workspace:
|
|
49
|
-
- `execute_bash`: `cp ~/.agend/workspaces/<source>/YYYYMMDD.json ~/.agend/workspaces/<target>/`
|
|
50
|
-
|
|
51
|
-
5. Wait for new instance to be idle, then load session via tmux:
|
|
52
|
-
- `execute_bash`: `tmux send-keys -t agend:<new-instance-name> '/chat load YYYYMMDD.json' Enter`
|
|
53
|
-
- Or configure `pre_task_command: "/chat load YYYYMMDD.json"` for auto-load on restart
|
|
54
|
-
|
|
55
|
-
## Claude Code Session Storage & Fork
|
|
56
|
-
|
|
57
|
-
claude-code stores conversation sessions as JSONL, **keyed by the project (working) directory**:
|
|
58
|
-
- **Path:** `~/.claude/projects/<project-path-encoded>/*.jsonl`
|
|
59
|
-
- `<project-path-encoded>` is the absolute working_directory with `/` replaced by `-`
|
|
60
|
-
(e.g. `/home/han/Projects/AgEnD` → `-home-han-Projects-AgEnD`)
|
|
61
|
-
- Each `.jsonl` is one session (full message history). Latest = most recently modified.
|
|
62
|
-
|
|
63
|
-
**Key difference from kiro-cli:** claude-code has **no `/chat save` / `/chat load`**. You resume
|
|
64
|
-
only via `--continue` (latest session for this dir) or `--resume <id>`. `/export` produces
|
|
65
|
-
plain text only — it **cannot** be reloaded as a session. So forking is done by **copying the
|
|
66
|
-
`.jsonl` file**, not by save/load commands.
|
|
8
|
+
| | kiro-cli | claude-code |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| Store | `~/.kiro/sessions/cli/<uuid>.json` | `~/.claude/projects/<path-encoded>/*.jsonl` |
|
|
11
|
+
| Keyed by | session uuid | project (working) directory |
|
|
12
|
+
| Reload | `/chat load <file>` | none — `--continue` (latest for the dir) / `--resume <id>` |
|
|
13
|
+
| Text export | — | `/export` (plain text, **not** reloadable) |
|
|
67
14
|
|
|
68
|
-
|
|
15
|
+
`<path-encoded>` = the absolute working_directory with `/` → `-`
|
|
16
|
+
(e.g. `/home/han/Projects/AgEnD` → `-home-han-Projects-AgEnD`).
|
|
69
17
|
|
|
70
|
-
|
|
71
|
-
(look for the ready prompt, e.g. `❯`). Don't fork mid-task.
|
|
18
|
+
## Fork a session to a new instance
|
|
72
19
|
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
ls -lt ~/.claude/projects/<source-path-encoded>/*.jsonl | head -5
|
|
76
|
-
```
|
|
20
|
+
Confirm the source is **idle** first (`describe_instance` / `get_fleet_status`) — don't fork mid-task.
|
|
77
21
|
|
|
78
|
-
|
|
79
|
-
|
|
22
|
+
**kiro-cli** — save, copy, load:
|
|
23
|
+
1. On the source, save: `/chat save <name>.json -f` (paste via tmux if needed).
|
|
24
|
+
2. `create_instance` (same backend).
|
|
25
|
+
3. `cp ~/.agend/workspaces/<source>/<name>.json ~/.agend/workspaces/<target>/`
|
|
26
|
+
4. Load on the target: `/chat load <name>.json`, or set `pre_task_command: "/chat load <name>.json"`.
|
|
80
27
|
|
|
81
|
-
|
|
28
|
+
**claude-code** — copy the `.jsonl` (no save/load command):
|
|
29
|
+
1. Newest source session: `ls -lt ~/.claude/projects/<source-encoded>/*.jsonl | head`
|
|
30
|
+
2. `create_instance` (backend `claude-code`); note its working_directory.
|
|
31
|
+
3. Copy into the target's encoded project dir:
|
|
82
32
|
```bash
|
|
83
33
|
TARGET_ENC="$(echo '<target-working-dir>' | sed 's#/#-#g')"
|
|
84
34
|
mkdir -p ~/.claude/projects/$TARGET_ENC
|
|
85
|
-
cp ~/.claude/projects/<source-
|
|
86
|
-
~/.claude/projects/$TARGET_ENC/
|
|
35
|
+
cp ~/.claude/projects/<source-encoded>/<session>.jsonl ~/.claude/projects/$TARGET_ENC/
|
|
87
36
|
```
|
|
37
|
+
4. Start the target — claude-code resumes the newest `.jsonl` via `--continue`.
|
|
88
38
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
### Caveats
|
|
93
|
-
- **Same project path required:** a session can only resume under the working_directory it
|
|
94
|
-
was recorded in. If the target dir differs, claude-code still loads it via `--continue`
|
|
95
|
-
(it reads the newest `.jsonl` in the target's encoded dir), but file paths/context inside
|
|
96
|
-
the transcript will refer to the original dir.
|
|
97
|
-
- `/export` = text only, not reloadable. Use the raw `.jsonl`.
|
|
98
|
-
- Pick the **right** `.jsonl` if multiple exist (sort by mtime; each branch/compaction can
|
|
99
|
-
create new files).
|
|
100
|
-
|
|
101
|
-
### kiro-cli vs claude-code fork (summary)
|
|
102
|
-
| | kiro-cli | claude-code |
|
|
103
|
-
|---|---|---|
|
|
104
|
-
| Store | `~/.kiro/sessions/cli/<uuid>.json` | `~/.claude/projects/<path-encoded>/*.jsonl` |
|
|
105
|
-
| Keyed by | session uuid | project (working) directory |
|
|
106
|
-
| Fork method | `/chat save` → copy → `/chat load` | copy `.jsonl` → `--continue` |
|
|
107
|
-
| Reload command | `/chat load <file>` | none — `--continue` / `--resume` only |
|
|
108
|
-
| Text export | — | `/export` (not reloadable) |
|
|
109
|
-
|
|
110
|
-
## Batch Session Backup
|
|
111
|
-
|
|
112
|
-
Save all instances' sessions to a dated backup directory:
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
DATE=$(date +%Y%m%d)
|
|
116
|
-
BACKUP_DIR="$HOME/.agend/session-backups/$DATE"
|
|
117
|
-
mkdir -p "$BACKUP_DIR"
|
|
118
|
-
MY_NAME="<your-own-instance-name>" # skip yourself to avoid paste collision
|
|
119
|
-
for win in $(tmux list-windows -t agend -F '#{window_name}' | grep -v bash); do
|
|
120
|
-
if [ "$win" = "$MY_NAME" ]; then continue; fi
|
|
121
|
-
tmux send-keys -t "agend:$win" "/chat save $BACKUP_DIR/${win}.json -f" Enter
|
|
122
|
-
sleep 3
|
|
123
|
-
done
|
|
124
|
-
```
|
|
39
|
+
**Caveats:** a claude-code session only truly makes sense under its original working_directory
|
|
40
|
+
(paths inside the transcript refer to it). Pick the right `.jsonl` if several exist (newest by
|
|
41
|
+
mtime; compaction/branches create new files). `/export` is text only, not reloadable.
|
|
125
42
|
|
|
126
|
-
|
|
127
|
-
- Skip your own instance (the one executing this) to avoid paste collision
|
|
128
|
-
- Use `sleep 3` between saves
|
|
129
|
-
- Run fleet health check first — only backup idle instances
|
|
130
|
-
- Do NOT backup while instances are busy
|
|
43
|
+
## Backup
|
|
131
44
|
|
|
132
|
-
|
|
133
|
-
|
|
45
|
+
Sessions are plain files — back them up by copying the store paths above
|
|
46
|
+
(e.g. `cp ~/.kiro/sessions/cli/*.json <dest>/`). Only copy while the instance is idle.
|
|
@@ -4,6 +4,14 @@ import type { Logger } from "./logger.js";
|
|
|
4
4
|
import type { IpcClient } from "./channel/ipc-bridge.js";
|
|
5
5
|
import type { EventLog } from "./event-log.js";
|
|
6
6
|
import type { TmuxControlClient } from "./tmux-control.js";
|
|
7
|
+
export interface BackendInstallationInfo {
|
|
8
|
+
binary: string;
|
|
9
|
+
install: string;
|
|
10
|
+
}
|
|
11
|
+
/** Shared CLI metadata used by startup validation and ClassicBot onboarding. */
|
|
12
|
+
export declare const BACKEND_INSTALLATION_INFO: Readonly<Record<string, BackendInstallationInfo>>;
|
|
13
|
+
/** Check one executable using the same PATH visible to the fleet process. */
|
|
14
|
+
export declare function checkBinaryInstalled(binary: string): boolean;
|
|
7
15
|
/**
|
|
8
16
|
* Context interface for instance lifecycle operations.
|
|
9
17
|
* FleetManager implements this.
|
|
@@ -20,6 +28,10 @@ export interface LifecycleContext {
|
|
|
20
28
|
readonly controlClient: TmuxControlClient | null;
|
|
21
29
|
getInstanceDir(name: string): string;
|
|
22
30
|
saveFleetConfig(): void;
|
|
31
|
+
/** Full stop+start. freshStart forces the respawn to skip session resume. */
|
|
32
|
+
restartSingleInstance(name: string, opts?: {
|
|
33
|
+
freshStart?: boolean;
|
|
34
|
+
}): Promise<void>;
|
|
23
35
|
connectIpcToInstance(name: string): Promise<void>;
|
|
24
36
|
createForumTopic(topicName: string, adapterId?: string): Promise<number | string>;
|
|
25
37
|
deleteForumTopic(topicId: number | string): Promise<void>;
|
|
@@ -27,7 +39,7 @@ export interface LifecycleContext {
|
|
|
27
39
|
/** Remove instance with full cleanup (scheduler, IPC, routing, config). */
|
|
28
40
|
removeInstance(name: string): Promise<void>;
|
|
29
41
|
touchActivity(name: string): void;
|
|
30
|
-
sendHangNotification(name: string): Promise<void>;
|
|
42
|
+
sendHangNotification(name: string, unchangedForMs?: number): Promise<void>;
|
|
31
43
|
notifyInstanceTopic(name: string, text: string): void;
|
|
32
44
|
/** List claimed tasks for an instance (from task board). Returns empty array if unavailable. */
|
|
33
45
|
listClaimedTasks(assignee: string): Array<{
|
|
@@ -39,7 +51,9 @@ export interface LifecycleContext {
|
|
|
39
51
|
/** Retire (delete) any pending Cancel button for an instance. No-op if none. */
|
|
40
52
|
clearCancelButton(name: string): void;
|
|
41
53
|
startStatuslineWatcher(name: string): void;
|
|
54
|
+
stopStatuslineWatcher(name: string): void;
|
|
42
55
|
reactMessageStatus(instanceName: string, chatId: string, messageId: string, emoji: string): void;
|
|
56
|
+
startPersistedPausedInstance(name: string): Promise<void>;
|
|
43
57
|
}
|
|
44
58
|
/** Arguments accepted by handleCreate — mirrors CreateInstanceArgs in outbound-schemas.ts
|
|
45
59
|
* plus internal-only fields forwarded by deploy_template (profile-derived). */
|
|
@@ -8,6 +8,27 @@ import { sanitizeInstanceName } from "./topic-commands.js";
|
|
|
8
8
|
import { isModelCompatible } from "./backend/types.js";
|
|
9
9
|
import { safeHandler } from "./safe-async.js";
|
|
10
10
|
import { t } from "./locale.js";
|
|
11
|
+
import { clearPausedMarker, hasPausedMarker, readPausedAt, writePausedMarker } from "./pause-marker.js";
|
|
12
|
+
/** Shared CLI metadata used by startup validation and ClassicBot onboarding. */
|
|
13
|
+
export const BACKEND_INSTALLATION_INFO = {
|
|
14
|
+
"claude-code": { binary: "claude", install: "curl -fsSL https://claude.ai/install.sh | bash" },
|
|
15
|
+
"gemini-cli": { binary: "gemini", install: "npm i -g @google/gemini-cli" },
|
|
16
|
+
"kiro-cli": { binary: "kiro-cli", install: "brew install --cask kiro-cli" },
|
|
17
|
+
codex: { binary: "codex", install: "npm i -g @openai/codex" },
|
|
18
|
+
opencode: { binary: "opencode", install: "curl -fsSL https://opencode.ai/install | bash" },
|
|
19
|
+
antigravity: { binary: "agy", install: "curl -fsSL https://antigravity.google/cli/install.sh | bash" },
|
|
20
|
+
grok: { binary: "grok", install: "curl -fsSL https://x.ai/cli/install.sh | bash" },
|
|
21
|
+
};
|
|
22
|
+
/** Check one executable using the same PATH visible to the fleet process. */
|
|
23
|
+
export function checkBinaryInstalled(binary) {
|
|
24
|
+
try {
|
|
25
|
+
execFileSync("which", [binary], { stdio: "pipe", timeout: 2000 });
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
11
32
|
export class InstanceLifecycle {
|
|
12
33
|
ctx;
|
|
13
34
|
/** Active daemon processes: instanceName → Daemon */
|
|
@@ -35,18 +56,9 @@ export class InstanceLifecycle {
|
|
|
35
56
|
const { createBackend } = await import("./backend/factory.js");
|
|
36
57
|
const backendName = config.backend ?? this.ctx.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
37
58
|
// Verify backend binary is in PATH before spawning
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
42
|
-
const bin = binaryMap[backendName];
|
|
43
|
-
if (bin) {
|
|
44
|
-
try {
|
|
45
|
-
execFileSync("which", [bin], { stdio: "pipe", timeout: 2000 });
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
this.ctx.logger.error({ binary: bin, backend: backendName, instance: name }, `Backend binary "${bin}" not found in PATH`);
|
|
49
|
-
}
|
|
59
|
+
const installation = BACKEND_INSTALLATION_INFO[backendName];
|
|
60
|
+
if (installation && !checkBinaryInstalled(installation.binary)) {
|
|
61
|
+
this.ctx.logger.error({ binary: installation.binary, backend: backendName, instance: name }, `Backend binary "${installation.binary}" not found in PATH`);
|
|
50
62
|
}
|
|
51
63
|
// claude-code refuses --dangerously-skip-permissions as root — fail fast
|
|
52
64
|
// Unless IS_SANDBOX=1 is set (recognized sandbox environment)
|
|
@@ -57,7 +69,7 @@ export class InstanceLifecycle {
|
|
|
57
69
|
return;
|
|
58
70
|
}
|
|
59
71
|
const backend = createBackend(backendName, instanceDir);
|
|
60
|
-
const daemon = new Daemon(name, config, instanceDir, topicMode, backend, this.ctx.controlClient ?? undefined);
|
|
72
|
+
const daemon = new Daemon(name, config, instanceDir, topicMode, backend, this.ctx.controlClient ?? undefined, this.ctx.logger);
|
|
61
73
|
// Catch errors from daemon internals (e.g. IPC server) to prevent crashing the fleet process
|
|
62
74
|
daemon.on("error", (err) => {
|
|
63
75
|
this.ctx.logger.error({ err, name }, "Daemon emitted error — instance isolated");
|
|
@@ -66,7 +78,7 @@ export class InstanceLifecycle {
|
|
|
66
78
|
this.daemons.set(name, daemon);
|
|
67
79
|
const hangDetector = daemon.getHangDetector();
|
|
68
80
|
if (hangDetector) {
|
|
69
|
-
hangDetector.on("hang", safeHandler(async () => {
|
|
81
|
+
hangDetector.on("hang", safeHandler(async (data) => {
|
|
70
82
|
this.ctx.eventLog?.insert(name, "hang_detected", {});
|
|
71
83
|
this.ctx.logger.warn({ name }, "Instance appears hung");
|
|
72
84
|
// Check if instance has claimed tasks — nudge it to continue
|
|
@@ -84,7 +96,7 @@ export class InstanceLifecycle {
|
|
|
84
96
|
});
|
|
85
97
|
}
|
|
86
98
|
}
|
|
87
|
-
await this.ctx.sendHangNotification(name);
|
|
99
|
+
await this.ctx.sendHangNotification(name, data?.unchangedForMs);
|
|
88
100
|
this.ctx.webhookEmit("hang", name);
|
|
89
101
|
}, this.ctx.logger, `hangDetector[${name}]`));
|
|
90
102
|
}
|
|
@@ -135,6 +147,13 @@ export class InstanceLifecycle {
|
|
|
135
147
|
if (data.action === "failover") {
|
|
136
148
|
this.ctx.checkModelFailover(name, 100); // Force failover trigger
|
|
137
149
|
}
|
|
150
|
+
else if (data.action === "restart") {
|
|
151
|
+
// A broken *resumed* session (e.g. agy pinned to a dead model) can't
|
|
152
|
+
// self-recover; a plain restart would --continue back into it. freshStart
|
|
153
|
+
// makes the respawn skip resume so the CLI starts a clean session on its
|
|
154
|
+
// default (valid) model.
|
|
155
|
+
this.ctx.restartSingleInstance(name, { freshStart: true }).catch(err => this.ctx.logger.error({ err, name }, "pty_error restart failed"));
|
|
156
|
+
}
|
|
138
157
|
}, this.ctx.logger, `daemon.pty_error[${name}]`));
|
|
139
158
|
daemon.on("pty_recovered", safeHandler((data) => {
|
|
140
159
|
const mins = Math.floor(data.downtime_s / 60);
|
|
@@ -164,22 +183,49 @@ export class InstanceLifecycle {
|
|
|
164
183
|
this.ctx.touchActivity(name);
|
|
165
184
|
}
|
|
166
185
|
isPaused(name) {
|
|
167
|
-
return this.daemons.get(name)?.isPaused ??
|
|
186
|
+
return this.daemons.get(name)?.isPaused ?? hasPausedMarker(this.ctx.getInstanceDir(name));
|
|
168
187
|
}
|
|
169
188
|
getLastPausedAt(name) {
|
|
170
|
-
return this.daemons.get(name)?.lastPausedAt ??
|
|
189
|
+
return this.daemons.get(name)?.lastPausedAt ?? readPausedAt(this.ctx.getInstanceDir(name));
|
|
171
190
|
}
|
|
172
191
|
async pause(name) {
|
|
173
192
|
const daemon = this.daemons.get(name);
|
|
174
|
-
if (!daemon)
|
|
193
|
+
if (!daemon) {
|
|
194
|
+
if (hasPausedMarker(this.ctx.getInstanceDir(name)))
|
|
195
|
+
return;
|
|
175
196
|
throw new Error(`Cannot pause stopped instance '${name}'`);
|
|
176
|
-
|
|
197
|
+
}
|
|
198
|
+
this.ctx.stopStatuslineWatcher(name);
|
|
199
|
+
try {
|
|
200
|
+
await daemon.pause();
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
// A rejected/no-op pause leaves the instance active and must not strand
|
|
204
|
+
// its fleet-level statusline watcher in the frozen state.
|
|
205
|
+
if (!daemon.isPaused)
|
|
206
|
+
this.ctx.startStatuslineWatcher(name);
|
|
207
|
+
}
|
|
177
208
|
}
|
|
178
209
|
async wake(name, timeoutMs = 30_000) {
|
|
179
210
|
const daemon = this.daemons.get(name);
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
211
|
+
if (daemon) {
|
|
212
|
+
await daemon.wake(timeoutMs);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
const instanceDir = this.ctx.getInstanceDir(name);
|
|
216
|
+
if (!hasPausedMarker(instanceDir))
|
|
217
|
+
throw new Error(`Cannot wake stopped instance '${name}'`);
|
|
218
|
+
const pausedAt = readPausedAt(instanceDir) ?? Date.now();
|
|
219
|
+
clearPausedMarker(instanceDir);
|
|
220
|
+
try {
|
|
221
|
+
await this.ctx.startPersistedPausedInstance(name);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
writePausedMarker(instanceDir, pausedAt);
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
this.ctx.startStatuslineWatcher(name);
|
|
183
229
|
}
|
|
184
230
|
async stop(name) {
|
|
185
231
|
this.ctx.setTopicIcon(name, "remove");
|
|
@@ -326,6 +372,7 @@ export class InstanceLifecycle {
|
|
|
326
372
|
// Remove from fleet config and save
|
|
327
373
|
delete this.ctx.fleetConfig.instances[name];
|
|
328
374
|
this.ctx.saveFleetConfig();
|
|
375
|
+
clearPausedMarker(this.ctx.getInstanceDir(name));
|
|
329
376
|
this.ctx.logger.info({ name }, "Instance removed");
|
|
330
377
|
}
|
|
331
378
|
/** Handle create_instance tool call from a daemon. */
|
|
@@ -474,6 +521,8 @@ export class InstanceLifecycle {
|
|
|
474
521
|
const explicitTopicName = args.topic_name;
|
|
475
522
|
const nameBase = explicitTopicName ?? (worktreePath ? topicName : (directory ? basename(workDir) : topicName));
|
|
476
523
|
newInstanceName = `${sanitizeInstanceName(nameBase)}-t${createdTopicId}`;
|
|
524
|
+
// A recycled name must never inherit a stale pause marker.
|
|
525
|
+
clearPausedMarker(this.ctx.getInstanceDir(newInstanceName));
|
|
477
526
|
// If no directory was provided, auto-create default workspace
|
|
478
527
|
if (!directory) {
|
|
479
528
|
workDir = join(getAgendHome(), "workspaces", newInstanceName);
|