dsh-agent-lines 0.3.6 → 0.3.8
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/CHANGELOG.en.md +15 -0
- package/CHANGELOG.md +15 -0
- package/CODE_OF_CONDUCT.en.md +0 -0
- package/CODE_OF_CONDUCT.md +0 -0
- package/CONTRIBUTING.en.md +0 -0
- package/CONTRIBUTING.md +0 -0
- package/LICENSE +0 -0
- package/NOTICE +0 -0
- package/README.en.md +4 -3
- package/README.md +9 -3
- package/SECURITY.en.md +0 -0
- package/SECURITY.md +0 -0
- package/bin/dsh-agent-lines.js +0 -0
- package/cordis.patch.yml +0 -0
- package/docs/ARCHITECTURE.en.md +0 -0
- package/docs/ARCHITECTURE.md +0 -0
- package/docs/DEPLOY.en.md +0 -0
- package/docs/DEPLOY.md +0 -0
- package/docs/PROTOCOL.en.md +1 -0
- package/docs/PROTOCOL.md +1 -0
- package/docs/SECURITY.en.md +0 -0
- package/docs/SECURITY.md +0 -0
- package/docs/assets/first-use.gif +0 -0
- package/docs/assets/social-preview.png +0 -0
- package/docs/en/index.html +4 -4
- package/docs/index.html +4 -4
- package/docs/market-index.json +3 -3
- package/examples/dashboard/README.md +16 -0
- package/examples/market-index.json +3 -3
- package/lib/activity.js +19 -0
- package/lib/delivery.js +0 -0
- package/lib/index.js +11 -2
- package/lib/prompt.js +0 -0
- package/lib/protocol.js +0 -0
- package/lib/store.js +27 -1
- package/lib/tools.js +66 -1
- package/lib/transport.js +0 -0
- package/package.json +1 -1
package/CHANGELOG.en.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
English | [中文](CHANGELOG.md)
|
|
4
4
|
|
|
5
|
+
## 0.3.8 (2026-11-09)
|
|
6
|
+
|
|
7
|
+
- line_status / line_list expose activity: turns/steps/decodeTokens/level/lastActivityAt.
|
|
8
|
+
- activityLevel is an activity proxy, not a context-window percentage; DSH does not expose context usage.
|
|
9
|
+
- Remains compatible with the 0.3.6/0.3.7 lifecycle/title work.
|
|
10
|
+
- Tests 35/35 pass.
|
|
11
|
+
|
|
12
|
+
## 0.3.7 (2026-11-09)
|
|
13
|
+
|
|
14
|
+
- Added line_handoff: structured handoff snapshots (JSON/Markdown), no model call.
|
|
15
|
+
- line_status / line_list now expose lifecycle/title/generation/pending/lastActivityAt/oldSessionId.
|
|
16
|
+
- rebind sends a noReply handoff notice to the new line; replace reuses the onboarding message.
|
|
17
|
+
- list --json can feed an external dashboard; added examples/dashboard notes.
|
|
18
|
+
- Tests 33/33 pass.
|
|
19
|
+
|
|
5
20
|
## 0.3.6 (2026-11-09)
|
|
6
21
|
|
|
7
22
|
- Added line lifecycle: line_retire, line_rebind, line_create replace:true, line_rename.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
[English](CHANGELOG.en.md) | 中文
|
|
4
4
|
|
|
5
|
+
## 0.3.8 (2026-11-09)
|
|
6
|
+
|
|
7
|
+
- line_status / line_list 增加 activity:turns/steps/decodeTokens/level/lastActivityAt。
|
|
8
|
+
- 明确 activityLevel 只是活跃度代理,不是 contextUsage 百分比;DSH 未暴露上下文窗口。
|
|
9
|
+
- 生命周期/标题/在飞版本保持 0.3.6/0.3.7 兼容。
|
|
10
|
+
- 测试 35/35 通过。
|
|
11
|
+
|
|
12
|
+
## 0.3.7 (2026-11-09)
|
|
13
|
+
|
|
14
|
+
- 新增 line_handoff:结构化交接快照(JSON/Markdown),不调用模型。
|
|
15
|
+
- line_status / line_list 增加 lifecycle/title/generation/pending/lastActivityAt/oldSessionId。
|
|
16
|
+
- rebind 后给新线发 noReply 交接通知;replace 复用 onboarding 消息。
|
|
17
|
+
- list --json 可直接喂给外部看板;新增 examples/dashboard 说明。
|
|
18
|
+
- 测试 33/33 通过。
|
|
19
|
+
|
|
5
20
|
## 0.3.6 (2026-11-09)
|
|
6
21
|
|
|
7
22
|
- 新增线生命周期:line_retire、line_rebind、line_create replace:true、line_rename。
|
package/CODE_OF_CONDUCT.en.md
CHANGED
|
File without changes
|
package/CODE_OF_CONDUCT.md
CHANGED
|
File without changes
|
package/CONTRIBUTING.en.md
CHANGED
|
File without changes
|
package/CONTRIBUTING.md
CHANGED
|
File without changes
|
package/LICENSE
CHANGED
|
File without changes
|
package/NOTICE
CHANGED
|
File without changes
|
package/README.en.md
CHANGED
|
@@ -36,7 +36,7 @@ The script picks the DSH Web profile, installs the plugin, and verifies agent-li
|
|
|
36
36
|
|
|
37
37
|
Offline or when npm is unavailable, use the local tarball:
|
|
38
38
|
|
|
39
|
-
bash install.sh ./dsh-agent-lines-0.3.
|
|
39
|
+
bash install.sh ./dsh-agent-lines-0.3.8.tgz
|
|
40
40
|
|
|
41
41
|
After install and restart, it works without a config. When you want a fixed manager/worker setup, paste the "Manager config" block below into the file printed by the installer.
|
|
42
42
|
|
|
@@ -134,10 +134,11 @@ Tools:
|
|
|
134
134
|
|
|
135
135
|
| Tool | Purpose |
|
|
136
136
|
|---|---|
|
|
137
|
-
| `line_status` | Read-only: current Session binding, manager Session live/persisted, lines, repair command; visible even when unbound |
|
|
137
|
+
| `line_status` | Read-only: current Session binding, manager Session live/persisted, lines, repair command; visible even when unbound; includes activity (not contextUsage) |
|
|
138
138
|
| `line_claim_manager` | Bind this live root Session as manager; requires `allowClaim: true` and `confirm: "bind-manager"`; global, no shell needed |
|
|
139
139
|
| `line_list` | List lines, status, unread counts, current line and role |
|
|
140
140
|
| `line_create` | Manager only: create a line; same label is idempotent; replace:true swaps the label to a fresh Session |
|
|
141
|
+
| `line_handoff` | Manager only: structured handoff snapshot (JSON/Markdown), no model call |
|
|
141
142
|
| `line_retire` | Manager only: soft-retire; reject new sends, keep Session and history |
|
|
142
143
|
| `line_rebind` | Manager only: atomically switch a line to a new live Session, optionally migrating backlog |
|
|
143
144
|
| `line_rename` | Manager only: rename the sidebar title and persist it in lines.db |
|
|
@@ -154,7 +155,7 @@ Line tools other than `line_status` and `line_claim_manager` are registered only
|
|
|
154
155
|
|
|
155
156
|
A line does not inherit the old Session context. Recommended flow:
|
|
156
157
|
|
|
157
|
-
1. Have the old line write a handoff file (line_handoff is available from 0.3.
|
|
158
|
+
1. Have the old line write a handoff file (line_handoff is available from 0.3.8);
|
|
158
159
|
2. The manager calls `line_create({ label, replace: true, force: true, reason: "context full" })`;
|
|
159
160
|
3. The plugin creates a clean Session, atomically swaps the same label, and marks the old Session retired but keeps it;
|
|
160
161
|
4. queued/claimed messages migrate to the new line; delivered/acked stay on the old Session and are never re-delivered.
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ DSH 的 super agent 平替插件。
|
|
|
36
36
|
|
|
37
37
|
离线或 npm 不可用时,用本地 tarball:
|
|
38
38
|
|
|
39
|
-
bash install.sh ./dsh-agent-lines-0.3.
|
|
39
|
+
bash install.sh ./dsh-agent-lines-0.3.8.tgz
|
|
40
40
|
|
|
41
41
|
安装并重启后,不写配置也可以直接使用;需要固定管理线/worker 时,再把下面“管理线配置”粘贴到脚本打印的配置文件。
|
|
42
42
|
|
|
@@ -124,6 +124,11 @@ line_create({ label: "ops", role: "运维" })
|
|
|
124
124
|
|
|
125
125
|
这只是“用提示词组织团队”的方式,不是插件内置的组织架构;怎么拆任务由负责人模型决定。技术细节(消息、状态、工具)在下面的“使用”和“投递语义”。
|
|
126
126
|
|
|
127
|
+
## 上下文活跃度
|
|
128
|
+
|
|
129
|
+
line_status / line_list 返回 activity:turns/steps/decodeTokens/level/lastActivityAt。
|
|
130
|
+
level 是活跃度代理(low/medium/high),不是上下文使用率百分比;DSH 当前不暴露上下文窗口。
|
|
131
|
+
|
|
127
132
|
## 使用
|
|
128
133
|
|
|
129
134
|
管理线可以完全由对话驱动:
|
|
@@ -134,10 +139,11 @@ line_create({ label: "ops", role: "运维" })
|
|
|
134
139
|
|
|
135
140
|
| 工具 | 作用 |
|
|
136
141
|
|---|---|
|
|
137
|
-
| `line_status` | 只读:当前会话绑定、manager Session 是否存在/live
|
|
142
|
+
| `line_status` | 只读:当前会话绑定、manager Session 是否存在/live、线列表、修复命令;未绑定也可见;含 activity 活跃度(不是 contextUsage) |
|
|
138
143
|
| `line_claim_manager` | 把当前 live 根 Session 绑成 manager;需要 `allowClaim: true` 和 `confirm: "bind-manager"`;全局工具,不需要 shell |
|
|
139
144
|
| `line_list` | 列出线、状态、未读数、当前线和 role |
|
|
140
145
|
| `line_create` | 仅管理线:创建新线;同 label 幂等;传 replace:true 可给同 label 换新 Session |
|
|
146
|
+
| `line_handoff` | 仅管理线:生成结构化交接单(JSON/Markdown),不调模型 |
|
|
141
147
|
| `line_retire` | 仅管理线:软下线;拒绝新发送,保留 Session/历史 |
|
|
142
148
|
| `line_rebind` | 仅管理线:把线原子切到新的 live Session,可迁移积压 |
|
|
143
149
|
| `line_rename` | 仅管理线:改侧栏标题,持久化到 lines.db |
|
|
@@ -154,7 +160,7 @@ line_create({ label: "ops", role: "运维" })
|
|
|
154
160
|
|
|
155
161
|
线不会自动继承旧 Session 的上下文。推荐流程:
|
|
156
162
|
|
|
157
|
-
1. 让旧线写交接单(0.3.
|
|
163
|
+
1. 让旧线写交接单(0.3.8 起可用 `line_handoff`);
|
|
158
164
|
2. 管理线调用 `line_create({ label, replace: true, force: true, reason: "context full" })`;
|
|
159
165
|
3. 插件创建干净 Session、同 label 原子切换,旧 Session 标记 retired 但保留;
|
|
160
166
|
4. queued/claimed 消息迁移到新线;delivered/acked 留在旧 Session,不重投。
|
package/SECURITY.en.md
CHANGED
|
File without changes
|
package/SECURITY.md
CHANGED
|
File without changes
|
package/bin/dsh-agent-lines.js
CHANGED
|
File without changes
|
package/cordis.patch.yml
CHANGED
|
File without changes
|
package/docs/ARCHITECTURE.en.md
CHANGED
|
File without changes
|
package/docs/ARCHITECTURE.md
CHANGED
|
File without changes
|
package/docs/DEPLOY.en.md
CHANGED
|
File without changes
|
package/docs/DEPLOY.md
CHANGED
|
File without changes
|
package/docs/PROTOCOL.en.md
CHANGED
|
@@ -103,6 +103,7 @@ The new line is an ordinary DSH Session in the same DSH process; no OS process,
|
|
|
103
103
|
line_retire({ line, reason? })
|
|
104
104
|
line_rebind({ line, sessionId, migrate?: "queued" | "none", reason? })
|
|
105
105
|
line_create({ label, replace: true, force?, migrate?, title?, reason? })
|
|
106
|
+
line_handoff({ line, format?: "md" | "json", limit?: number })
|
|
106
107
|
line_rename({ line, title })
|
|
107
108
|
|
|
108
109
|
- retired: line_send returns LINE_RETIRED; the Session and history are kept.
|
package/docs/PROTOCOL.md
CHANGED
|
@@ -95,6 +95,7 @@ line_move_workspace({ line, workspace }) 把已有线 Session 挂到已有工作
|
|
|
95
95
|
line_retire({ line, reason? })
|
|
96
96
|
line_rebind({ line, sessionId, migrate?: "queued" | "none", reason? })
|
|
97
97
|
line_create({ label, replace: true, force?, migrate?, title?, reason? })
|
|
98
|
+
line_handoff({ line, format?: "md" | "json", limit?: number })
|
|
98
99
|
line_rename({ line, title })
|
|
99
100
|
|
|
100
101
|
- retired:line_send 返回 LINE_RETIRED;Session 和消息历史保留。
|
package/docs/SECURITY.en.md
CHANGED
|
File without changes
|
package/docs/SECURITY.md
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/docs/en/index.html
CHANGED
|
@@ -43,7 +43,7 @@ footer{padding:34px 0 48px;color:var(--muted);font-size:12px}
|
|
|
43
43
|
<div class="wrap">
|
|
44
44
|
<header>
|
|
45
45
|
<a class="brand" href="../"><span class="mark"></span>dsh-agent-lines</a>
|
|
46
|
-
<nav><a href="../">中文</a><a href="https://github.com/Lencen/dsh-agent-lines">GitHub</a><a href="https://github.com/Lencen/dsh-agent-lines/releases/tag/v0.3.
|
|
46
|
+
<nav><a href="../">中文</a><a href="https://github.com/Lencen/dsh-agent-lines">GitHub</a><a href="https://github.com/Lencen/dsh-agent-lines/releases/tag/v0.3.8">Release</a></nav>
|
|
47
47
|
</header>
|
|
48
48
|
<main>
|
|
49
49
|
<section class="hero" style="border:0;padding-bottom:22px">
|
|
@@ -54,7 +54,7 @@ footer{padding:34px 0 48px;color:var(--muted);font-size:12px}
|
|
|
54
54
|
<div class="install">curl -fsSL https://raw.githubusercontent.com/Lencen/dsh-agent-lines/main/install.sh -o install.sh && bash install.sh</div>
|
|
55
55
|
<div class="actions">
|
|
56
56
|
<a class="btn primary" href="https://github.com/Lencen/dsh-agent-lines">GitHub repository</a>
|
|
57
|
-
<a class="btn" href="https://github.com/Lencen/dsh-agent-lines/releases/tag/v0.3.
|
|
57
|
+
<a class="btn" href="https://github.com/Lencen/dsh-agent-lines/releases/tag/v0.3.8">Download 0.3.8</a>
|
|
58
58
|
</div>
|
|
59
59
|
</div>
|
|
60
60
|
<div class="shot">
|
|
@@ -92,8 +92,8 @@ research (调研), architecture (架构), uiux (UIUX), dev (开发), test (测
|
|
|
92
92
|
After the team is ready, wait for my first task. Do not start by yourself.</pre>
|
|
93
93
|
<p style="color:var(--muted);font-size:14px">Restart DSH Web after install or update. Technical details, protocol, and limits: <a href="https://github.com/Lencen/dsh-agent-lines">README / architecture</a>. Community plugin, not an official DeepSeek product.</p>
|
|
94
94
|
</section>
|
|
95
|
-
<footer>dsh-agent-lines 0.3.
|
|
95
|
+
<footer>dsh-agent-lines 0.3.8 · Apache-2.0 · Community plugin for DeepSeek Harness. Not an official DeepSeek product.</footer>
|
|
96
96
|
</main>
|
|
97
97
|
</div>
|
|
98
98
|
</body>
|
|
99
|
-
</html>
|
|
99
|
+
</html>
|
package/docs/index.html
CHANGED
|
@@ -43,7 +43,7 @@ footer{padding:34px 0 48px;color:var(--muted);font-size:12px}
|
|
|
43
43
|
<div class="wrap">
|
|
44
44
|
<header>
|
|
45
45
|
<a class="brand" href="./"><span class="mark"></span>dsh-agent-lines</a>
|
|
46
|
-
<nav><a href="en/index.html">English</a><a href="https://github.com/Lencen/dsh-agent-lines">GitHub</a><a href="https://github.com/Lencen/dsh-agent-lines/releases/tag/v0.3.
|
|
46
|
+
<nav><a href="en/index.html">English</a><a href="https://github.com/Lencen/dsh-agent-lines">GitHub</a><a href="https://github.com/Lencen/dsh-agent-lines/releases/tag/v0.3.8">Release</a></nav>
|
|
47
47
|
</header>
|
|
48
48
|
<main>
|
|
49
49
|
<section class="hero" style="border:0;padding-bottom:22px">
|
|
@@ -54,7 +54,7 @@ footer{padding:34px 0 48px;color:var(--muted);font-size:12px}
|
|
|
54
54
|
<div class="install">curl -fsSL https://raw.githubusercontent.com/Lencen/dsh-agent-lines/main/install.sh -o install.sh && bash install.sh</div>
|
|
55
55
|
<div class="actions">
|
|
56
56
|
<a class="btn primary" href="https://github.com/Lencen/dsh-agent-lines">GitHub 仓库</a>
|
|
57
|
-
<a class="btn" href="https://github.com/Lencen/dsh-agent-lines/releases/tag/v0.3.
|
|
57
|
+
<a class="btn" href="https://github.com/Lencen/dsh-agent-lines/releases/tag/v0.3.8">下载 0.3.8</a>
|
|
58
58
|
</div>
|
|
59
59
|
</div>
|
|
60
60
|
<div class="shot">
|
|
@@ -92,8 +92,8 @@ research(调研)、architecture(架构)、uiux(UIUX)、dev(开发
|
|
|
92
92
|
团队建好后先待命,不要自动开工,等我发第一个任务。</pre>
|
|
93
93
|
<p style="color:var(--muted);font-size:14px">安装或更新后需要重启 DSH Web。技术细节、协议和限制见 <a href="https://github.com/Lencen/dsh-agent-lines">README / 架构文档</a>。社区插件,不是 DeepSeek 官方产品。</p>
|
|
94
94
|
</section>
|
|
95
|
-
<footer>dsh-agent-lines 0.3.
|
|
95
|
+
<footer>dsh-agent-lines 0.3.8 · Apache-2.0 · Community plugin for DeepSeek Harness. Not an official DeepSeek product.</footer>
|
|
96
96
|
</main>
|
|
97
97
|
</div>
|
|
98
98
|
</body>
|
|
99
|
-
</html>
|
|
99
|
+
</html>
|
package/docs/market-index.json
CHANGED
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
]
|
|
51
51
|
},
|
|
52
52
|
"dist-tags": {
|
|
53
|
-
"latest": "0.3.
|
|
53
|
+
"latest": "0.3.8"
|
|
54
54
|
},
|
|
55
55
|
"versions": [
|
|
56
56
|
{
|
|
57
|
-
"version": "0.3.
|
|
57
|
+
"version": "0.3.8",
|
|
58
58
|
"publishTimestamp": 1793771000,
|
|
59
59
|
"source": {
|
|
60
60
|
"type": "npm"
|
|
@@ -66,4 +66,4 @@
|
|
|
66
66
|
]
|
|
67
67
|
}
|
|
68
68
|
]
|
|
69
|
-
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Dashboard example
|
|
2
|
+
|
|
3
|
+
The plugin deliberately does not run an HTTP server. Read the same SQLite queue with:
|
|
4
|
+
|
|
5
|
+
~~~bash
|
|
6
|
+
dsh-agent-lines --json list
|
|
7
|
+
~~~
|
|
8
|
+
|
|
9
|
+
Fields: label/title/role/lifecycle/generation/pending/unread/lastActivityAt/lease.
|
|
10
|
+
The Surface feedback dashboard (refresh-dashboard.js) can consume this JSON and keep its own directory scan.
|
|
11
|
+
|
|
12
|
+
For a handoff snapshot before replacing a line:
|
|
13
|
+
|
|
14
|
+
~~~text
|
|
15
|
+
line_handoff({ line: "writer", format: "json" })
|
|
16
|
+
~~~
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
]
|
|
51
51
|
},
|
|
52
52
|
"dist-tags": {
|
|
53
|
-
"latest": "0.3.
|
|
53
|
+
"latest": "0.3.8"
|
|
54
54
|
},
|
|
55
55
|
"versions": [
|
|
56
56
|
{
|
|
57
|
-
"version": "0.3.
|
|
57
|
+
"version": "0.3.8",
|
|
58
58
|
"publishTimestamp": 1793771000,
|
|
59
59
|
"source": {
|
|
60
60
|
"type": "npm"
|
|
@@ -66,4 +66,4 @@
|
|
|
66
66
|
]
|
|
67
67
|
}
|
|
68
68
|
]
|
|
69
|
-
}
|
|
69
|
+
}
|
package/lib/activity.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Pure activity summarizer. Never claims a context-window percentage. */
|
|
2
|
+
export function activityLevel({ steps = 0, turns = 0 } = {}) {
|
|
3
|
+
const n = Number(steps) || 0
|
|
4
|
+
if (n >= 40) return 'high'
|
|
5
|
+
if (n >= 15) return 'medium'
|
|
6
|
+
return 'low'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function summarizeActivity(stats, lastActivityAt = null) {
|
|
10
|
+
if (stats === undefined || stats === null) return null
|
|
11
|
+
const turns = Number(stats.turns) || 0
|
|
12
|
+
const steps = Number(stats.steps) || 0
|
|
13
|
+
return {
|
|
14
|
+
turns, steps,
|
|
15
|
+
decodeTokens: Number(stats.decodeTokens) || 0,
|
|
16
|
+
level: activityLevel({ steps, turns }),
|
|
17
|
+
lastActivityAt: lastActivityAt === null ? null : Number(lastActivityAt),
|
|
18
|
+
}
|
|
19
|
+
}
|
package/lib/delivery.js
CHANGED
|
File without changes
|
package/lib/index.js
CHANGED
|
@@ -297,7 +297,7 @@ export async function apply(ctx, rawConfig) {
|
|
|
297
297
|
|
|
298
298
|
const lineTitle = (def, row) => String((row && row.title) || (def && def.title) || (def && def.role) || (def && def.label) || 'agent line').slice(0, 120)
|
|
299
299
|
|
|
300
|
-
const rebindLine = async ({ label, sessionId, actor, reason, migrate = 'queued', force = false }) => {
|
|
300
|
+
const rebindLine = async ({ label, sessionId, actor, reason, migrate = 'queued', force = false, notify = true }) => {
|
|
301
301
|
if (label === config.createFrom) throw new LineError('LINE_MANAGER_REBIND_UNSUPPORTED', 'The manager line is rebound with line_claim_manager, not line_rebind', { line: label })
|
|
302
302
|
const row = store.getLineRow(config.agentId, label)
|
|
303
303
|
if (row === undefined) throw new LineError('LINE_NOT_FOUND', `No such line: ${config.agentId}#${label}`)
|
|
@@ -332,6 +332,15 @@ export async function apply(ctx, rawConfig) {
|
|
|
332
332
|
startLoop(def)
|
|
333
333
|
const agent = ctx.agents.get(sessionId)
|
|
334
334
|
if (agent !== undefined) bindAgent(agent)
|
|
335
|
+
if (notify !== false) {
|
|
336
|
+
try {
|
|
337
|
+
store.send({
|
|
338
|
+
agentId: config.agentId, fromLine: config.createFrom, to: label,
|
|
339
|
+
text: `[Agent Line] ${label} switched to a fresh Session. old=${row.session_id} new=${sessionId} migrated=${JSON.stringify(updated.counts)}. Read the handoff file before continuing.`,
|
|
340
|
+
noReply: true, maxReplyDepth: config.maxReplyDepth, maxMessageBytes: config.maxMessageBytes,
|
|
341
|
+
})
|
|
342
|
+
} catch (error) { ctx.logger?.warn?.('agent-lines: rebind notification failed: %s', String(error)) }
|
|
343
|
+
}
|
|
335
344
|
return { line: label, oldSessionId: row.session_id, newSessionId: sessionId, generation: updated.generation, counts: updated.counts }
|
|
336
345
|
} catch (error) {
|
|
337
346
|
try { await stopLoopFor(label) } catch { /* best effort */ }
|
|
@@ -397,7 +406,7 @@ export async function apply(ctx, rawConfig) {
|
|
|
397
406
|
const actualSessionId = created.sessionId
|
|
398
407
|
const result = await rebindLine({
|
|
399
408
|
label: existing.label, sessionId: actualSessionId, actor: (exec && exec.agent && exec.agent.session && exec.agent.session.id) || null,
|
|
400
|
-
reason: String(args.reason || 'line_create replace'), migrate: args.migrate === 'none' ? 'none' : 'queued', force: args.force === true,
|
|
409
|
+
reason: String(args.reason || 'line_create replace'), migrate: args.migrate === 'none' ? 'none' : 'queued', force: args.force === true, notify: false,
|
|
401
410
|
})
|
|
402
411
|
const newDef = lineDefs.get(actualSessionId)
|
|
403
412
|
if (newDef !== undefined) {
|
package/lib/prompt.js
CHANGED
|
File without changes
|
package/lib/protocol.js
CHANGED
|
File without changes
|
package/lib/store.js
CHANGED
|
@@ -410,13 +410,19 @@ export class LineStore {
|
|
|
410
410
|
/** All lines with unread counts and live lease state. */
|
|
411
411
|
listLines(now = Date.now()) {
|
|
412
412
|
const rows = this.#stmt(`SELECT l.*, (SELECT COUNT(*) FROM messages m
|
|
413
|
-
WHERE m.to_agent = l.agent_id AND m.to_line = l.label AND m.state IN ('queued', 'claimed', 'delivered')) AS unread
|
|
413
|
+
WHERE m.to_agent = l.agent_id AND m.to_line = l.label AND m.state IN ('queued', 'claimed', 'delivered')) AS unread,
|
|
414
|
+
(SELECT COUNT(*) FROM messages m
|
|
415
|
+
WHERE m.to_agent = l.agent_id AND m.to_line = l.label AND m.state IN ('queued', 'claimed')) AS pending,
|
|
416
|
+
(SELECT MAX(m.created_at) FROM messages m
|
|
417
|
+
WHERE m.to_agent = l.agent_id AND m.to_line = l.label) AS last_activity_at
|
|
414
418
|
FROM lines l ORDER BY l.agent_id, l.label`).all()
|
|
415
419
|
return rows.map(row => {
|
|
416
420
|
const lease = this.#stmt('SELECT * FROM leases WHERE line_id = ?').get(row.line_id)
|
|
417
421
|
return {
|
|
418
422
|
...row,
|
|
419
423
|
unread: Number(row.unread),
|
|
424
|
+
pending: Number(row.pending ?? 0),
|
|
425
|
+
lastActivityAt: row.last_activity_at === null ? null : Number(row.last_activity_at),
|
|
420
426
|
lease: lease === undefined ? null : {
|
|
421
427
|
instanceId: lease.instance_id,
|
|
422
428
|
expiresAt: lease.expires_at,
|
|
@@ -433,9 +439,13 @@ export class LineStore {
|
|
|
433
439
|
if (row === undefined) return undefined
|
|
434
440
|
const lease = this.#stmt('SELECT * FROM leases WHERE line_id = ?').get(id)
|
|
435
441
|
const unread = this.#stmt(`SELECT COUNT(*) AS n FROM messages WHERE to_agent = ? AND to_line = ? AND state IN ('queued', 'claimed', 'delivered')`).get(agentId, line)
|
|
442
|
+
const pending = this.#stmt(`SELECT COUNT(*) AS n FROM messages WHERE to_agent = ? AND to_line = ? AND state IN ('queued', 'claimed')`).get(agentId, line)
|
|
443
|
+
const activity = this.#stmt('SELECT MAX(created_at) AS n FROM messages WHERE to_agent = ? AND to_line = ?').get(agentId, line)
|
|
436
444
|
return {
|
|
437
445
|
...row,
|
|
438
446
|
unread: Number(unread.n),
|
|
447
|
+
pending: Number(pending.n),
|
|
448
|
+
lastActivityAt: activity.n === null ? null : Number(activity.n),
|
|
439
449
|
lease: lease === undefined ? null : { instanceId: lease.instance_id, expiresAt: lease.expires_at, live: lease.expires_at > now },
|
|
440
450
|
}
|
|
441
451
|
}
|
|
@@ -532,6 +542,22 @@ export class LineStore {
|
|
|
532
542
|
return this.#stmt('SELECT * FROM line_events WHERE line_id = ? AND action = ? ORDER BY created_at DESC LIMIT 1').get(id, action)
|
|
533
543
|
}
|
|
534
544
|
|
|
545
|
+
/** Structured handoff snapshot: counts + recent messages + lifecycle events. */
|
|
546
|
+
handoff({ agentId, line, limit = 20, now = Date.now() }) {
|
|
547
|
+
const row = this.getLine(agentId, line, now)
|
|
548
|
+
if (row === undefined) throw new LineError('LINE_NOT_FOUND', `No such line: ${lineId(agentId, line)}`)
|
|
549
|
+
const recent = this.#stmt(`SELECT * FROM messages WHERE to_agent = ? AND to_line = ? ORDER BY created_at DESC LIMIT ?`).all(agentId, line, limit).reverse()
|
|
550
|
+
const events = this.lineEvents({ agentId, line, limit: 10 })
|
|
551
|
+
const lastEvent = events.length ? events[0] : null
|
|
552
|
+
return {
|
|
553
|
+
line: line, label: line, role: row.role, title: row.title ?? row.role ?? row.label,
|
|
554
|
+
sessionId: row.session_id, lifecycle: row.lifecycle ?? 'active', generation: row.generation ?? 1,
|
|
555
|
+
createdBy: row.created_by, createdAt: row.created_at, counts: this.messageCounts({ agentId, line }),
|
|
556
|
+
lastActivityAt: row.lastActivityAt, oldSessionId: lastEvent && lastEvent.action === 'rebind' ? lastEvent.old_session_id : null,
|
|
557
|
+
lastEvent, events, messages: recent,
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
535
561
|
/** Recent lifecycle events for diagnostics. */
|
|
536
562
|
lineEvents({ agentId, line, limit = 20 }) {
|
|
537
563
|
const id = lineId(agentId, line)
|
package/lib/tools.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from 'node:crypto'
|
|
7
7
|
import { LineError, assertMessageSize, lineId, parseTarget } from './protocol.js'
|
|
8
|
+
import { summarizeActivity } from './activity.js'
|
|
8
9
|
|
|
9
10
|
const OUTPUT = {
|
|
10
11
|
schema: { type: 'object', additionalProperties: true },
|
|
@@ -48,6 +49,16 @@ function materializationOf(ctx, sessionId) {
|
|
|
48
49
|
} catch { return null }
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
function activityOf(ctx, sessionId) {
|
|
53
|
+
const projections = ctx.get?.('sessionProjections')
|
|
54
|
+
const agent = ctx.agents.get(sessionId)
|
|
55
|
+
if (projections?.snapshot === undefined || agent === undefined) return null
|
|
56
|
+
try {
|
|
57
|
+
const snapshot = projections.snapshot(agent.session, ['sessionStats'])
|
|
58
|
+
return summarizeActivity(snapshot?.values?.sessionStats, null)
|
|
59
|
+
} catch { return null }
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
function currentView(def) {
|
|
52
63
|
return def === undefined ? null : {
|
|
53
64
|
agentId: def.agentId, line: def.label, sessionId: def.sessionId, lineId: def.lineId, role: def.role,
|
|
@@ -98,11 +109,15 @@ export function registerStatusTool(ctx, { config, store, lineDefs }) {
|
|
|
98
109
|
title: line.title ?? line.role ?? line.label,
|
|
99
110
|
lifecycle: line.lifecycle ?? 'active',
|
|
100
111
|
generation: line.generation ?? 1,
|
|
112
|
+
oldSessionId: store.lastLineEvent({ agentId: line.agent_id, line: line.label, action: 'rebind' })?.old_session_id ?? null,
|
|
113
|
+
pending: line.pending ?? 0,
|
|
114
|
+
lastActivityAt: line.lastActivityAt ?? null,
|
|
101
115
|
sessionId: line.session_id,
|
|
102
116
|
createdBy: line.created_by,
|
|
103
117
|
lease: line.lease,
|
|
104
118
|
live: ctx.agents.get(line.session_id) !== undefined,
|
|
105
119
|
materialized: materializationOf(ctx, line.session_id),
|
|
120
|
+
activity: activityOf(ctx, line.session_id),
|
|
106
121
|
}))
|
|
107
122
|
const current = currentId === undefined
|
|
108
123
|
? null
|
|
@@ -175,7 +190,7 @@ export function registerToolsForAgent(agent, def, { config, store, lineDefs, cre
|
|
|
175
190
|
const current = lineDefs.get(exec?.agent?.session?.id)
|
|
176
191
|
return {
|
|
177
192
|
current: currentView(current),
|
|
178
|
-
lines: store.listLines().map(line => ({ ...line, live: agent.ctx.agents.get(line.session_id) !== undefined, materialized: materializationOf(agent.ctx, line.session_id) })),
|
|
193
|
+
lines: store.listLines().map(line => ({ ...line, live: agent.ctx.agents.get(line.session_id) !== undefined, materialized: materializationOf(agent.ctx, line.session_id), activity: activityOf(agent.ctx, line.session_id) })),
|
|
179
194
|
bootstrapOnCreate: config.bootstrapOnCreate,
|
|
180
195
|
}
|
|
181
196
|
},
|
|
@@ -383,6 +398,56 @@ export function registerToolsForAgent(agent, def, { config, store, lineDefs, cre
|
|
|
383
398
|
},
|
|
384
399
|
}))
|
|
385
400
|
|
|
401
|
+
disposers.push(agent.ctx.tools.register({
|
|
402
|
+
name: 'line_handoff',
|
|
403
|
+
description: 'Generate a structured handoff snapshot for a line: current metadata, backlog counts, recent messages, lifecycle events. Does not call a model; write the returned markdown to a file before retire/replace/rebind.',
|
|
404
|
+
parameters: {
|
|
405
|
+
type: 'object', additionalProperties: false,
|
|
406
|
+
properties: {
|
|
407
|
+
line: { type: 'string', description: 'Existing line label' },
|
|
408
|
+
format: { type: 'string', enum: ['md', 'json'], description: 'md (default) returns markdown; json returns the snapshot object' },
|
|
409
|
+
limit: { type: 'number', description: 'Recent messages to include, 1-100 (default 20)' },
|
|
410
|
+
},
|
|
411
|
+
required: ['line'],
|
|
412
|
+
},
|
|
413
|
+
output: OUTPUT,
|
|
414
|
+
execute: async (args, exec) => {
|
|
415
|
+
const caller = currentLine(exec, lineDefs)
|
|
416
|
+
if (caller.label !== config.createFrom) throw new LineError('LINE_CREATE_FORBIDDEN', `Only ${config.createFrom} may create handoffs`, { caller: caller.label })
|
|
417
|
+
const limit = limitValue(args.limit, 20, 100)
|
|
418
|
+
const data = store.handoff({ agentId: config.agentId, line: String(args.line), limit })
|
|
419
|
+
if (args.format === 'json') return data
|
|
420
|
+
const lines = [
|
|
421
|
+
'# 交接单 · ' + data.label,
|
|
422
|
+
'',
|
|
423
|
+
'## 1 我是谁',
|
|
424
|
+
'- 角色: ' + (data.role ?? data.label),
|
|
425
|
+
'- 当前 label: ' + data.label,
|
|
426
|
+
'- Session: ' + data.sessionId,
|
|
427
|
+
'- lifecycle: ' + data.lifecycle + ' (generation ' + data.generation + ')',
|
|
428
|
+
'',
|
|
429
|
+
'## 2 积压',
|
|
430
|
+
'- queued: ' + data.counts.queued,
|
|
431
|
+
'- claimed: ' + data.counts.claimed,
|
|
432
|
+
'- delivered: ' + data.counts.delivered,
|
|
433
|
+
'- acked: ' + data.counts.acked,
|
|
434
|
+
'- failed/expired: ' + (data.counts.failed + data.counts.expired),
|
|
435
|
+
'',
|
|
436
|
+
'## 3 最近消息',
|
|
437
|
+
...(data.messages.length === 0 ? ['- 无'] : data.messages.map(m => `- ${new Date(m.created_at).toISOString()} [${m.state}] ${m.from_line} -> ${m.to_line}: ${String(m.body).replace(/\s+/g, ' ').slice(0, 180)}`)),
|
|
438
|
+
'',
|
|
439
|
+
'## 4 生命周期',
|
|
440
|
+
...(data.events.length === 0 ? ['- 无'] : data.events.map(e => `- ${new Date(e.created_at).toISOString()} ${e.action} ${e.old_session_id ? e.old_session_id + ' -> ' : ''}${e.new_session_id ?? ''}`)),
|
|
441
|
+
'',
|
|
442
|
+
'## 5 交接提示',
|
|
443
|
+
'- 先读本文件,再继续工作。',
|
|
444
|
+
'- 不要复用旧 Session;它只保留历史。',
|
|
445
|
+
'- 如有 queued/claimed,新线会继续收到。',
|
|
446
|
+
]
|
|
447
|
+
return { line: data.label, format: 'md', markdown: lines.join('\n'), counts: data.counts, sessionId: data.sessionId, generation: data.generation }
|
|
448
|
+
},
|
|
449
|
+
}))
|
|
450
|
+
|
|
386
451
|
disposers.push(agent.ctx.tools.register({
|
|
387
452
|
name: 'line_move_workspace',
|
|
388
453
|
description: 'Move an existing line Session into a target workspace record so it appears under that workspace in the sidebar. DSH only allows attachment when the Session cwd exactly matches the workspace path; it cannot move a Session to a different directory (use a new line for that).',
|
package/lib/transport.js
CHANGED
|
File without changes
|