@bolloon/bolloon-agent 0.3.51 → 0.4.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/README.md +135 -135
- package/bin/bolloon-cli.cjs +183 -183
- package/bin/bolloon.cjs +0 -0
- package/bin/bolloon.js +157 -0
- package/dist/agents/constraint-layer.js +19 -19
- package/dist/agents/judgment-protocol.js +14 -14
- package/dist/agents/loop-review.js +21 -0
- package/dist/agents/pi-sdk-tools.js +86 -2
- package/dist/agents/pi-sdk-types.js +12 -12
- package/dist/agents/pi-sdk.js +90 -90
- package/dist/agents/shell-guard.js +22 -1
- package/dist/agents/skill-organizer.js +19 -19
- package/dist/agents/verify-recipe.js +84 -0
- package/dist/agents/workflow-pivot-loop.js +101 -13
- package/dist/bollharness/src/scripts/deploy-guard.js +8 -8
- package/dist/bollharness-integration/context-router-judgment.js +4 -4
- package/dist/bollharness-integration/context-router.js +292 -292
- package/dist/bollharness-integration/gate-state-machine.js +13 -13
- package/dist/bollharness-integration/llm-judgment-engine.js +134 -134
- package/dist/bollharness-integration/skill-adapter.js +21 -21
- package/dist/bootstrap/chat-archiver.js +14 -14
- package/dist/bootstrap/context-os.js +50 -40
- package/dist/bootstrap/memory-compressor.js +13 -13
- package/dist/cli-entry.js +36 -36
- package/dist/context-compaction/auto-compact.js +7 -7
- package/dist/electron/first-run.js +54 -54
- package/dist/electron-build/electron/first-run.js +54 -54
- package/dist/electron-preload.js.map +1 -1
- package/dist/electron.js.map +1 -1
- package/dist/external-engines/delegate-handle.js +70 -0
- package/dist/external-engines/delegate.js +24 -3
- package/dist/index.js +112 -112
- package/dist/llm/error-lessons.js +53 -0
- package/dist/llm/llm-judgment-client.js +102 -102
- package/dist/llm/pi-ai.js +61 -48
- package/dist/llm/system-prompt/layers/channel/human-async.md +41 -41
- package/dist/llm/system-prompt/layers/channel/local.md +14 -14
- package/dist/llm/system-prompt/layers/channel/p2p-agent.md +18 -18
- package/dist/llm/system-prompt/layers/channel/p2p-peer-sync.md +51 -51
- package/dist/llm/system-prompt/layers/channel/p2p-proactive.md +43 -43
- package/dist/llm/system-prompt/layers/channel/p2p-visitor.md +19 -19
- package/dist/llm/system-prompt/layers/channel/session-handoff.md +61 -61
- package/dist/llm/system-prompt/layers/core/artifacts_storage.md +89 -89
- package/dist/llm/system-prompt/layers/core/evenhandedness.md +21 -21
- package/dist/llm/system-prompt/layers/core/external-engagement.md +73 -73
- package/dist/llm/system-prompt/layers/core/hibs_reminders.md +15 -15
- package/dist/llm/system-prompt/layers/core/identity.md +48 -48
- package/dist/llm/system-prompt/layers/core/knowledge.md +17 -17
- package/dist/llm/system-prompt/layers/core/memory_system.md +12 -12
- package/dist/llm/system-prompt/layers/core/network_filesystem.md +28 -28
- package/dist/llm/system-prompt/layers/core/refusal.md +37 -37
- package/dist/llm/system-prompt/layers/core/tone.md +31 -31
- package/dist/llm/system-prompt/layers/core/tools.thin.md +13 -13
- package/dist/llm/system-prompt/layers/core/wellbeing.md +41 -41
- package/dist/llm/system-prompt/layers/role/architect.md +20 -20
- package/dist/llm/system-prompt/layers/role/expert.md +19 -19
- package/dist/llm/system-prompt/layers/role/implementer.md +15 -15
- package/dist/llm/system-prompt/layers/role/security.md +15 -15
- package/dist/llm/system-prompt/layers/tool/artifacts.md +72 -72
- package/dist/llm/system-prompt/layers/tool/bash.md +25 -25
- package/dist/llm/system-prompt/layers/tool/goal_handoff.md +77 -77
- package/dist/llm/system-prompt/layers/tool/hibs_api.md +171 -171
- package/dist/llm/system-prompt/layers/tool/image_search.md +70 -70
- package/dist/llm/system-prompt/layers/tool/manifest.md +89 -89
- package/dist/llm/system-prompt/layers/tool/mcp_apps.md +53 -53
- package/dist/llm/system-prompt/layers/tool/p2p_request.md +61 -61
- package/dist/llm/system-prompt/layers/tool/web_search.md +83 -83
- package/dist/llm/system-prompt/registry.js +9 -1
- package/dist/llm/tool-manifest/ask_user_input.js +1 -1
- package/dist/llm/tool-manifest/bash.js +3 -3
- package/dist/llm/tool-manifest/create_file.js +4 -4
- package/dist/llm/tool-manifest/fetch_sports_data.js +3 -3
- package/dist/llm/tool-manifest/image_search.js +2 -2
- package/dist/llm/tool-manifest/index.js +0 -59
- package/dist/llm/tool-manifest/mcp.js +2 -2
- package/dist/llm/tool-manifest/message_compose.js +3 -3
- package/dist/llm/tool-manifest/places.js +2 -2
- package/dist/llm/tool-manifest/present_files.js +1 -1
- package/dist/llm/tool-manifest/recipe.js +2 -2
- package/dist/llm/tool-manifest/str_replace.js +5 -5
- package/dist/llm/tool-manifest/view.js +3 -3
- package/dist/llm/tool-manifest/weather.js +4 -4
- package/dist/llm/tool-manifest/web.js +4 -4
- package/dist/locales/en.js +28 -0
- package/dist/locales/zh.js +26 -0
- package/dist/pi-ecosystem-colony/index.js +365 -0
- package/dist/pi-ecosystem-judgment/causal-judge.js +51 -51
- package/dist/pi-ecosystem-judgment/distill-prompt.js +42 -42
- package/dist/pi-ecosystem-judgment/distillation.js +14 -14
- package/dist/pi-ecosystem-judgment/monitor-gate.js +28 -28
- package/dist/pi-ecosystem-judgment/value-injection.js +102 -102
- package/dist/security/context-router-tool.js +15 -15
- package/dist/social/ant-colony/AdaptiveHeartbeat.js +101 -0
- package/dist/social/ant-colony/PheromoneEngine.js +227 -0
- package/dist/social/ant-colony/index.js +6 -0
- package/dist/social/ant-colony/types.js +24 -0
- package/dist/storage/trajectory.js +101 -0
- package/dist/test/ai-judgment-test.js +80 -0
- package/dist/test/bollharness-integration.test.js +318 -0
- package/dist/test/channel-agent-multi-dialogue.js +205 -0
- package/dist/test/channel-heartbeat-agent-test.js +201 -0
- package/dist/test/constraint-layer.test.js +164 -0
- package/dist/test/diap-identity-test.js +172 -0
- package/dist/test/diap-quick-test.js +62 -0
- package/dist/test/global-shared-context.test.js +315 -0
- package/dist/test/harness-judgment-injection.test.js +246 -0
- package/dist/test/harness-workflow-integrator-test.js +228 -0
- package/dist/test/human-value-store.test.js +243 -0
- package/dist/test/hybrid-integration-test.js +118 -0
- package/dist/test/hybrid-messenger-verify.js +55 -0
- package/dist/test/iroh-bistream-debug.js +38 -0
- package/dist/test/iroh-communication.test.js +66 -0
- package/dist/test/iroh-debug-test.js +57 -0
- package/dist/test/iroh-diap-test.js +71 -0
- package/dist/test/iroh-direct-connect.js +55 -0
- package/dist/test/iroh-e2e-fixed.js +89 -0
- package/dist/test/iroh-e2e-same-process.js +63 -0
- package/dist/test/iroh-e2e.js +66 -0
- package/dist/test/iroh-final-e2e.js +72 -0
- package/dist/test/iroh-relay-test.js +37 -0
- package/dist/test/iroh-simple-test.js +41 -0
- package/dist/test/iroh-transport-verify.js +54 -0
- package/dist/test/iroh-transport.test.js +37 -0
- package/dist/test/iroh-two-nodes.js +70 -0
- package/dist/test/iroh-verify.js +44 -0
- package/dist/test/judgment-decision.test.js +219 -0
- package/dist/test/llm-judgment-integration.test.js +220 -0
- package/dist/test/p2p-agent-complex-dialogue.js +385 -0
- package/dist/test/p2p-agent-dialogue.js +341 -0
- package/dist/test/p2p-agent-full-bidirectional.js +510 -0
- package/dist/test/p2p-agent-harness-flow.js +437 -0
- package/dist/test/p2p-agent-harness-single.js +143 -0
- package/dist/test/p2p-ai-dialogue-test.js +318 -0
- package/dist/test/p2p-cid-connect-test.js +195 -0
- package/dist/test/p2p-connect-receiver.js +69 -0
- package/dist/test/p2p-doc-transfer.js +110 -0
- package/dist/test/p2p-identity-page-test.js +77 -0
- package/dist/test/p2p-iroh-test.js +171 -0
- package/dist/test/p2p-minimal-test.js +241 -0
- package/dist/test/p2p-node-1.js +148 -0
- package/dist/test/p2p-node-2.js +148 -0
- package/dist/test/p2p-server.js +281 -0
- package/dist/test/p2p-two-nodes-test.js +438 -0
- package/dist/test/pi-sdk.test.js +44 -0
- package/dist/test/set-persona.js +40 -0
- package/dist/test/simple.test.js +9 -0
- package/dist/test/storage-integration.test.js +150 -0
- package/dist/test/subagent-manager.test.js +276 -0
- package/dist/test/test-gate-flow.test.js +81 -0
- package/dist/test/workflow-engine.test.js +87 -0
- package/dist/test/workflow-pivot-loop.test.js +246 -0
- package/dist/web/api-config.html +779 -779
- package/dist/web/client-hearth.js +10 -10
- package/dist/web/client.js +54 -0
- package/dist/web/components/wallet-viem.mjs +118 -118
- package/dist/web/delivery-ledger.js +103 -0
- package/dist/web/i18n.js +104 -0
- package/dist/web/index.html +472 -472
- package/dist/web/manifest.json +20 -20
- package/dist/web/routes-tasks.js +242 -24
- package/dist/web/server-storage.js +111 -0
- package/dist/web/server.js +55 -26
- package/dist/web/style.css +4946 -4946
- package/dist/web/task-breaker.js +18 -0
- package/dist/web/task-cancel.js +35 -0
- package/dist/web/task-deps.js +36 -0
- package/dist/web/task-review.js +27 -0
- package/dist/web/task-verify.js +60 -0
- package/package.json +196 -193
- package/scripts/build-cli.js +215 -215
- package/scripts/build-web.ts +130 -130
- package/scripts/postinstall.js +152 -152
- package/bin/bolloon-daemon.sh +0 -207
- package/dist/constraint-runtime/reference_data/archive_surface_snapshot.json +0 -63
- package/dist/constraint-runtime/reference_data/commands_snapshot.json +0 -1037
- package/dist/constraint-runtime/reference_data/subsystems/assistant.json +0 -8
- package/dist/constraint-runtime/reference_data/subsystems/bootstrap.json +0 -8
- package/dist/constraint-runtime/reference_data/subsystems/bridge.json +0 -32
- package/dist/constraint-runtime/reference_data/subsystems/buddy.json +0 -13
- package/dist/constraint-runtime/reference_data/subsystems/cli.json +0 -26
- package/dist/constraint-runtime/reference_data/subsystems/components.json +0 -32
- package/dist/constraint-runtime/reference_data/subsystems/constants.json +0 -28
- package/dist/constraint-runtime/reference_data/subsystems/coordinator.json +0 -8
- package/dist/constraint-runtime/reference_data/subsystems/entrypoints.json +0 -15
- package/dist/constraint-runtime/reference_data/subsystems/hooks.json +0 -32
- package/dist/constraint-runtime/reference_data/subsystems/keybindings.json +0 -21
- package/dist/constraint-runtime/reference_data/subsystems/memdir.json +0 -15
- package/dist/constraint-runtime/reference_data/subsystems/migrations.json +0 -18
- package/dist/constraint-runtime/reference_data/subsystems/moreright.json +0 -8
- package/dist/constraint-runtime/reference_data/subsystems/native_ts.json +0 -11
- package/dist/constraint-runtime/reference_data/subsystems/outputStyles.json +0 -8
- package/dist/constraint-runtime/reference_data/subsystems/plugins.json +0 -9
- package/dist/constraint-runtime/reference_data/subsystems/remote.json +0 -11
- package/dist/constraint-runtime/reference_data/subsystems/schemas.json +0 -8
- package/dist/constraint-runtime/reference_data/subsystems/screens.json +0 -10
- package/dist/constraint-runtime/reference_data/subsystems/server.json +0 -10
- package/dist/constraint-runtime/reference_data/subsystems/services.json +0 -32
- package/dist/constraint-runtime/reference_data/subsystems/skills.json +0 -27
- package/dist/constraint-runtime/reference_data/subsystems/state.json +0 -13
- package/dist/constraint-runtime/reference_data/subsystems/types.json +0 -18
- package/dist/constraint-runtime/reference_data/subsystems/upstreamproxy.json +0 -9
- package/dist/constraint-runtime/reference_data/subsystems/utils.json +0 -32
- package/dist/constraint-runtime/reference_data/subsystems/vim.json +0 -12
- package/dist/constraint-runtime/reference_data/subsystems/voice.json +0 -8
- package/dist/constraint-runtime/reference_data/tools_snapshot.json +0 -922
- package/dist/electron/config.js.map +0 -1
- package/dist/electron/dialogs.js.map +0 -1
- package/dist/electron/first-run.js.map +0 -1
- package/dist/electron/ipc.js.map +0 -1
- package/dist/electron/logger.js.map +0 -1
- package/dist/electron/main.js.map +0 -1
- package/dist/electron/menu.js.map +0 -1
- package/dist/electron/paths.js.map +0 -1
- package/dist/electron/server.js.map +0 -1
- package/dist/electron/tray.js.map +0 -1
- package/dist/electron/window.js.map +0 -1
- package/dist/utils/auto-update.js.map +0 -1
- package/dist/web/client.js.map +0 -7
package/dist/web/manifest.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "Bolloon Agent",
|
|
3
|
-
"short_name": "Bolloon",
|
|
4
|
-
"description": "AI Agent with Claude API integration",
|
|
5
|
-
"start_url": "/",
|
|
6
|
-
"display": "standalone",
|
|
7
|
-
"background_color": "#1a1a2e",
|
|
8
|
-
"theme_color": "#1a1a2e",
|
|
9
|
-
"icons": [
|
|
10
|
-
{
|
|
11
|
-
"src": "/icons/favicon-192x192.png",
|
|
12
|
-
"sizes": "192x192",
|
|
13
|
-
"type": "image/png"
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"src": "/icons/favicon-512x512.png",
|
|
17
|
-
"sizes": "512x512",
|
|
18
|
-
"type": "image/png"
|
|
19
|
-
}
|
|
20
|
-
]
|
|
1
|
+
{
|
|
2
|
+
"name": "Bolloon Agent",
|
|
3
|
+
"short_name": "Bolloon",
|
|
4
|
+
"description": "AI Agent with Claude API integration",
|
|
5
|
+
"start_url": "/",
|
|
6
|
+
"display": "standalone",
|
|
7
|
+
"background_color": "#1a1a2e",
|
|
8
|
+
"theme_color": "#1a1a2e",
|
|
9
|
+
"icons": [
|
|
10
|
+
{
|
|
11
|
+
"src": "/icons/favicon-192x192.png",
|
|
12
|
+
"sizes": "192x192",
|
|
13
|
+
"type": "image/png"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"src": "/icons/favicon-512x512.png",
|
|
17
|
+
"sizes": "512x512",
|
|
18
|
+
"type": "image/png"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
21
|
}
|
package/dist/web/routes-tasks.js
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* 从 src/web/server.ts 抽出 (~160 行).
|
|
5
5
|
* 包含 /api/tasks/* (GET, POST, PATCH, DELETE, execute, execute-next)
|
|
6
6
|
*/
|
|
7
|
-
import { loadTaskQueue, saveTaskQueue
|
|
7
|
+
import { loadTaskQueue, saveTaskQueue } from './server-storage.js';
|
|
8
8
|
import { documentReader } from '../documents/reader.js';
|
|
9
|
+
import { applyCancelRequest, shouldFinalizeAsCancelled } from './task-cancel.js';
|
|
10
|
+
import { applyReviewRequest, applyReviewApprove, applyReviewReject } from './task-review.js';
|
|
9
11
|
export function registerTaskRoutes(app, opts) {
|
|
10
12
|
const { broadcast, getAgentForChannel } = opts;
|
|
11
13
|
// 获取所有任务
|
|
@@ -21,10 +23,19 @@ export function registerTaskRoutes(app, opts) {
|
|
|
21
23
|
// 创建新任务
|
|
22
24
|
app.post('/api/tasks', async (req, res) => {
|
|
23
25
|
try {
|
|
24
|
-
const { type, title, description, steps } = req.body;
|
|
26
|
+
const { type, title, description, steps, parentIds } = req.body;
|
|
25
27
|
if (!type || !title) {
|
|
26
28
|
return res.status(400).json({ error: 'type and title required' });
|
|
27
29
|
}
|
|
30
|
+
// 2026-08-11 (Hermes 父依赖): parentIds 必须是已存在的任务 id
|
|
31
|
+
if (parentIds !== undefined) {
|
|
32
|
+
const tasks0 = await loadTaskQueue();
|
|
33
|
+
const known = new Set(tasks0.map((t) => t.id));
|
|
34
|
+
const unknown = (Array.isArray(parentIds) ? parentIds : []).filter((id) => !known.has(id));
|
|
35
|
+
if (unknown.length > 0) {
|
|
36
|
+
return res.status(400).json({ error: `Unknown parent task ids: ${unknown.join(', ')}` });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
28
39
|
const tasks = await loadTaskQueue();
|
|
29
40
|
const task = {
|
|
30
41
|
id: `task_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
|
|
@@ -35,6 +46,7 @@ export function registerTaskRoutes(app, opts) {
|
|
|
35
46
|
progress: 0,
|
|
36
47
|
createdAt: new Date().toISOString(),
|
|
37
48
|
updatedAt: new Date().toISOString(),
|
|
49
|
+
parentIds: Array.isArray(parentIds) && parentIds.length > 0 ? parentIds : undefined,
|
|
38
50
|
steps: steps?.map((s, i) => ({
|
|
39
51
|
id: `step_${i}`,
|
|
40
52
|
name: s,
|
|
@@ -109,14 +121,24 @@ export function registerTaskRoutes(app, opts) {
|
|
|
109
121
|
if (!channelId) {
|
|
110
122
|
return res.status(400).json({ error: 'channelId required' });
|
|
111
123
|
}
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
if (isTaskExecuting()) {
|
|
124
|
+
// 2026-08-11: CAS 认领 — 只有 pending 能认领成功, 输家不重试
|
|
125
|
+
const { claimTaskForExecution } = await import('./server-storage.js');
|
|
126
|
+
const claim = await claimTaskForExecution(taskId);
|
|
127
|
+
if (claim === 'busy') {
|
|
118
128
|
return res.status(409).json({ error: 'Another task is currently executing' });
|
|
119
129
|
}
|
|
130
|
+
if (claim === 'not-pending') {
|
|
131
|
+
const tasks = await loadTaskQueue();
|
|
132
|
+
const task = tasks.find(t => t.id === taskId);
|
|
133
|
+
return res.status(409).json({ error: `Task not claimable (status=${task?.status ?? 'missing'})` });
|
|
134
|
+
}
|
|
135
|
+
if (claim === 'parents-undone') {
|
|
136
|
+
const tasks = await loadTaskQueue();
|
|
137
|
+
const task = tasks.find(t => t.id === taskId);
|
|
138
|
+
return res.status(409).json({ error: `Task parents not done (${(task?.parentIds || []).join(', ')}) — 父任务完成后才能执行` });
|
|
139
|
+
}
|
|
140
|
+
const tasks = await loadTaskQueue();
|
|
141
|
+
const task = tasks.find(t => t.id === taskId);
|
|
120
142
|
// 异步执行任务
|
|
121
143
|
executeTask(task, channelId, getAgentForChannel, broadcast);
|
|
122
144
|
res.json({ ok: true, taskId: task.id });
|
|
@@ -132,13 +154,11 @@ export function registerTaskRoutes(app, opts) {
|
|
|
132
154
|
if (!channelId) {
|
|
133
155
|
return res.status(400).json({ error: 'channelId required' });
|
|
134
156
|
}
|
|
135
|
-
|
|
136
|
-
const
|
|
157
|
+
// 2026-08-11: CAS 认领下一个 pending — 输家 (无任务/已被认领/忙) 返回, 不重试
|
|
158
|
+
const { claimNextPendingTask } = await import('./server-storage.js');
|
|
159
|
+
const nextTask = await claimNextPendingTask();
|
|
137
160
|
if (!nextTask) {
|
|
138
|
-
return res.json({ ok: false, message: 'No pending tasks' });
|
|
139
|
-
}
|
|
140
|
-
if (isTaskExecuting()) {
|
|
141
|
-
return res.status(409).json({ error: 'Another task is currently executing' });
|
|
161
|
+
return res.json({ ok: false, message: 'No pending tasks or another task is executing' });
|
|
142
162
|
}
|
|
143
163
|
// 异步执行任务
|
|
144
164
|
executeTask(nextTask, channelId, getAgentForChannel, broadcast);
|
|
@@ -148,13 +168,132 @@ export function registerTaskRoutes(app, opts) {
|
|
|
148
168
|
res.status(500).json({ error: err.message });
|
|
149
169
|
}
|
|
150
170
|
});
|
|
171
|
+
// 取消任务 (2026-08-11, Hermes 两段式: cancel-requested → cancelled)
|
|
172
|
+
// - pending 任务: 从未开始, 直接终态 cancelled (请求即完成, 不会卡在中间态)
|
|
173
|
+
// - running 任务: 先置 cancel-requested (第一段: 请求已受理), executor 观测到后置 cancelled (第二段: 实际停止)
|
|
174
|
+
app.post('/api/tasks/:taskId/cancel', async (req, res) => {
|
|
175
|
+
try {
|
|
176
|
+
const { taskId } = req.params;
|
|
177
|
+
const { channelId } = req.body || {};
|
|
178
|
+
const tasks = await loadTaskQueue();
|
|
179
|
+
const task = tasks.find(t => t.id === taskId);
|
|
180
|
+
if (!task) {
|
|
181
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
182
|
+
}
|
|
183
|
+
switch (task.status) {
|
|
184
|
+
case 'pending': {
|
|
185
|
+
const t = applyCancelRequest(task.status);
|
|
186
|
+
task.status = t.status;
|
|
187
|
+
task.updatedAt = new Date().toISOString();
|
|
188
|
+
await saveTaskQueue(tasks);
|
|
189
|
+
broadcast({ type: 'task_status', taskId: task.id, status: t.status, progress: task.progress }, channelId);
|
|
190
|
+
return res.json({ ok: true, taskId: task.id, status: t.status, phase: t.phase });
|
|
191
|
+
}
|
|
192
|
+
case 'running': {
|
|
193
|
+
const t = applyCancelRequest(task.status);
|
|
194
|
+
task.status = t.status;
|
|
195
|
+
task.updatedAt = new Date().toISOString();
|
|
196
|
+
await saveTaskQueue(tasks);
|
|
197
|
+
broadcast({ type: 'task_status', taskId: task.id, status: t.status }, channelId);
|
|
198
|
+
return res.json({ ok: true, taskId: task.id, status: t.status, phase: t.phase });
|
|
199
|
+
}
|
|
200
|
+
case 'cancel-requested':
|
|
201
|
+
return res.json({ ok: true, taskId: task.id, status: 'cancel-requested', phase: 'already-requested' });
|
|
202
|
+
default:
|
|
203
|
+
return res.status(409).json({ error: `Task already terminal (${task.status})` });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
res.status(500).json({ error: err.message });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
// ==================== review 审批通道 (2026-08-11, Hermes request_review 模式) ====================
|
|
211
|
+
// running/pending → review (挂起等人工/审查者) → approve (review→completed) / reject (review→pending 退回队列)
|
|
212
|
+
// 请求审批
|
|
213
|
+
app.post('/api/tasks/:taskId/review', async (req, res) => {
|
|
214
|
+
try {
|
|
215
|
+
const { taskId } = req.params;
|
|
216
|
+
const { channelId } = req.body || {};
|
|
217
|
+
const tasks = await loadTaskQueue();
|
|
218
|
+
const task = tasks.find(t => t.id === taskId);
|
|
219
|
+
if (!task) {
|
|
220
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
221
|
+
}
|
|
222
|
+
const t = applyReviewRequest(task.status);
|
|
223
|
+
if (t.phase === 'not-reviewable') {
|
|
224
|
+
return res.status(409).json({ error: `Task not reviewable (status=${task.status})` });
|
|
225
|
+
}
|
|
226
|
+
task.status = t.status;
|
|
227
|
+
task.updatedAt = new Date().toISOString();
|
|
228
|
+
await saveTaskQueue(tasks);
|
|
229
|
+
broadcast({ type: 'task_status', taskId: task.id, status: t.status }, channelId);
|
|
230
|
+
return res.json({ ok: true, taskId: task.id, status: t.status, phase: t.phase });
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
res.status(500).json({ error: err.message });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
// 审批通过 (放行完成)
|
|
237
|
+
app.post('/api/tasks/:taskId/approve', async (req, res) => {
|
|
238
|
+
try {
|
|
239
|
+
const { taskId } = req.params;
|
|
240
|
+
const { channelId } = req.body || {};
|
|
241
|
+
const tasks = await loadTaskQueue();
|
|
242
|
+
const task = tasks.find(t => t.id === taskId);
|
|
243
|
+
if (!task) {
|
|
244
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
245
|
+
}
|
|
246
|
+
const t = applyReviewApprove(task.status);
|
|
247
|
+
if (t.phase === 'not-reviewable') {
|
|
248
|
+
return res.status(409).json({ error: `Task not in review (status=${task.status})` });
|
|
249
|
+
}
|
|
250
|
+
task.status = t.status;
|
|
251
|
+
task.progress = 100;
|
|
252
|
+
task.updatedAt = new Date().toISOString();
|
|
253
|
+
await saveTaskQueue(tasks);
|
|
254
|
+
broadcast({ type: 'task_status', taskId: task.id, status: t.status, progress: 100 }, channelId);
|
|
255
|
+
broadcast({ type: 'status', content: `任务审批通过: ${task.title}` }, channelId);
|
|
256
|
+
return res.json({ ok: true, taskId: task.id, status: t.status, phase: t.phase });
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
res.status(500).json({ error: err.message });
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
// 审批驳回 (退回队列)
|
|
263
|
+
app.post('/api/tasks/:taskId/reject', async (req, res) => {
|
|
264
|
+
try {
|
|
265
|
+
const { taskId } = req.params;
|
|
266
|
+
const { channelId } = req.body || {};
|
|
267
|
+
const tasks = await loadTaskQueue();
|
|
268
|
+
const task = tasks.find(t => t.id === taskId);
|
|
269
|
+
if (!task) {
|
|
270
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
271
|
+
}
|
|
272
|
+
const t = applyReviewReject(task.status);
|
|
273
|
+
if (t.phase === 'not-reviewable') {
|
|
274
|
+
return res.status(409).json({ error: `Task not in review (status=${task.status})` });
|
|
275
|
+
}
|
|
276
|
+
task.status = t.status;
|
|
277
|
+
task.progress = 0;
|
|
278
|
+
task.result = undefined;
|
|
279
|
+
task.updatedAt = new Date().toISOString();
|
|
280
|
+
await saveTaskQueue(tasks);
|
|
281
|
+
broadcast({ type: 'task_status', taskId: task.id, status: t.status, progress: 0 }, channelId);
|
|
282
|
+
broadcast({ type: 'status', content: `任务审批驳回, 退回队列: ${task.title}` }, channelId);
|
|
283
|
+
return res.json({ ok: true, taskId: task.id, status: t.status, phase: t.phase });
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
res.status(500).json({ error: err.message });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
151
289
|
}
|
|
152
290
|
// ==================== Task Execution ====================
|
|
153
291
|
async function executeTask(task, channelId, getAgentForChannel, broadcast) {
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
292
|
+
// 2026-08-11: 认领已由 claimTaskForExecution/claimNextPendingTask (CAS) 完成并持锁,
|
|
293
|
+
// 这里不再重复 startTaskExecution (会因 isExecutingTask=true 返回 false 导致提前 return).
|
|
294
|
+
const { endTaskExecution, heartbeatTaskExecution } = await import('./server-storage.js');
|
|
295
|
+
// 2026-08-11 (Hermes 心跳): 开始执行先续期一次 — 认领后到首个 await 之间有调度延迟
|
|
296
|
+
await heartbeatTaskExecution(task.id).catch(() => { });
|
|
158
297
|
const agent = await getAgentForChannel(channelId);
|
|
159
298
|
const tasks = await loadTaskQueue();
|
|
160
299
|
const taskIndex = tasks.findIndex(t => t.id === task.id);
|
|
@@ -193,6 +332,29 @@ async function executeTask(task, channelId, getAgentForChannel, broadcast) {
|
|
|
193
332
|
default:
|
|
194
333
|
result = '未知任务类型';
|
|
195
334
|
}
|
|
335
|
+
// 2026-08-11 (Hermes 两段式): executor 观测到 cancel-requested → 落终态 cancelled (第二段)
|
|
336
|
+
// 运行中被请求取消 → 不执行正常完成路径, 直接标记取消
|
|
337
|
+
const tasksAfter = await loadTaskQueue();
|
|
338
|
+
const idxAfter = tasksAfter.findIndex(t => t.id === task.id);
|
|
339
|
+
if (idxAfter >= 0 && shouldFinalizeAsCancelled(tasksAfter[idxAfter].status)) {
|
|
340
|
+
tasksAfter[idxAfter].status = 'cancelled';
|
|
341
|
+
tasksAfter[idxAfter].result = '任务已取消';
|
|
342
|
+
tasksAfter[idxAfter].updatedAt = new Date().toISOString();
|
|
343
|
+
await saveTaskQueue(tasksAfter);
|
|
344
|
+
broadcast({ type: 'task_status', taskId: task.id, status: 'cancelled', progress: tasksAfter[idxAfter].progress }, channelId);
|
|
345
|
+
broadcast({ type: 'status', content: `任务已取消: ${task.title}` }, channelId);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
// 2026-08-11 (Hermes request_review 模式): 执行完成但任务已挂入 review 审批通道 →
|
|
349
|
+
// 只落结果, 不覆盖成 completed; 等 approve 路由放行 (review → completed)
|
|
350
|
+
if (idxAfter >= 0 && tasksAfter[idxAfter].status === 'review') {
|
|
351
|
+
tasksAfter[idxAfter].result = result;
|
|
352
|
+
tasksAfter[idxAfter].updatedAt = new Date().toISOString();
|
|
353
|
+
await saveTaskQueue(tasksAfter);
|
|
354
|
+
broadcast({ type: 'task_status', taskId: task.id, status: 'review', progress: tasksAfter[idxAfter].progress }, channelId);
|
|
355
|
+
broadcast({ type: 'status', content: `任务执行完成, 待审批: ${task.title}` }, channelId);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
196
358
|
// 更新任务状态
|
|
197
359
|
const tasks = await loadTaskQueue();
|
|
198
360
|
const idx = tasks.findIndex(t => t.id === task.id);
|
|
@@ -200,23 +362,79 @@ async function executeTask(task, channelId, getAgentForChannel, broadcast) {
|
|
|
200
362
|
tasks[idx].status = 'completed';
|
|
201
363
|
tasks[idx].progress = 100;
|
|
202
364
|
tasks[idx].result = result;
|
|
365
|
+
// 2026-08-11 (熔断器): 成功完成 → 连续失败计数清零
|
|
366
|
+
tasks[idx].consecutiveFailures = 0;
|
|
367
|
+
tasks[idx].lastFailureError = undefined;
|
|
368
|
+
tasks[idx].claimExpires = undefined;
|
|
203
369
|
tasks[idx].updatedAt = new Date().toISOString();
|
|
204
370
|
await saveTaskQueue(tasks);
|
|
205
371
|
}
|
|
206
372
|
broadcast({ type: 'task_status', taskId: task.id, status: 'completed', progress: 100, result }, channelId);
|
|
207
373
|
broadcast({ type: 'ai', content: result }, channelId);
|
|
374
|
+
// 2026-08-11 (Hermes 完成防幻觉, advisory): 异步校验 result 里的任务/文件引用,
|
|
375
|
+
// 幽灵引用 → 存 task.warnings + broadcast warning (不阻塞完成 — 单次同步执行无法返工)
|
|
376
|
+
void (async () => {
|
|
377
|
+
try {
|
|
378
|
+
const { verifyTaskResult } = await import('./task-verify.js');
|
|
379
|
+
const allTasks = await loadTaskQueue();
|
|
380
|
+
const v = await verifyTaskResult(result, { knownTaskIds: allTasks.map((t) => t.id) });
|
|
381
|
+
if (v.warnings.length > 0) {
|
|
382
|
+
const q = await loadTaskQueue();
|
|
383
|
+
const vi = q.findIndex((t) => t.id === task.id);
|
|
384
|
+
if (vi >= 0) {
|
|
385
|
+
q[vi].warnings = v.warnings;
|
|
386
|
+
await saveTaskQueue(q);
|
|
387
|
+
}
|
|
388
|
+
for (const w of v.warnings) {
|
|
389
|
+
broadcast({ type: 'error', content: `⚠ ${w}` }, channelId);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// 校验失败不阻塞完成
|
|
395
|
+
}
|
|
396
|
+
})();
|
|
208
397
|
}
|
|
209
398
|
catch (error) {
|
|
210
399
|
const tasks = await loadTaskQueue();
|
|
211
400
|
const idx = tasks.findIndex(t => t.id === task.id);
|
|
212
401
|
if (idx >= 0) {
|
|
213
|
-
tasks[idx]
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
402
|
+
const t = tasks[idx];
|
|
403
|
+
// 取消请求在先 → 异常属于取消副作用, 落 cancelled 而非 failed (Hermes 两段式)
|
|
404
|
+
if (t.status === 'cancel-requested') {
|
|
405
|
+
t.status = 'cancelled';
|
|
406
|
+
t.error = error.message;
|
|
407
|
+
t.updatedAt = new Date().toISOString();
|
|
408
|
+
await saveTaskQueue(tasks);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
// 2026-08-11 (Hermes consecutive_failures 熔断器): 失败计数 +1;
|
|
412
|
+
// 达到阈值 → failed 终态 (熔断, 不再自动重试)
|
|
413
|
+
// 未达阈值 → 回 pending 可重新认领 (execute-next 自动重试), 防一次性小错卡死队列
|
|
414
|
+
const { recordTaskFailure } = await import('./task-breaker.js');
|
|
415
|
+
const envLimit = Number(process.env.BOLLOON_FAILURE_LIMIT || '');
|
|
416
|
+
const f = recordTaskFailure(t, envLimit > 0 ? envLimit : undefined);
|
|
417
|
+
t.consecutiveFailures = f.consecutiveFailures;
|
|
418
|
+
t.lastFailureError = String(error.message || error).slice(0, 300);
|
|
419
|
+
t.claimExpires = undefined;
|
|
420
|
+
if (f.tripped) {
|
|
421
|
+
t.status = 'failed';
|
|
422
|
+
t.error = `连续失败 ${f.consecutiveFailures} 次, 熔断 (不再自动重试): ${String(error.message || error).slice(0, 200)}`;
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
t.status = 'pending';
|
|
426
|
+
t.error = `失败 ${f.consecutiveFailures} 次 (自动重试): ${String(error.message || error).slice(0, 200)}`;
|
|
427
|
+
}
|
|
428
|
+
t.updatedAt = new Date().toISOString();
|
|
429
|
+
await saveTaskQueue(tasks);
|
|
430
|
+
}
|
|
217
431
|
}
|
|
218
|
-
|
|
219
|
-
|
|
432
|
+
// 读取实际落态 (failed 熔断 / pending 自动重试 / cancelled)
|
|
433
|
+
const tasksNow = await loadTaskQueue();
|
|
434
|
+
const taskNow = tasksNow.find(t => t.id === task.id);
|
|
435
|
+
const finalStatus = taskNow?.status ?? 'failed';
|
|
436
|
+
broadcast({ type: 'task_status', taskId: task.id, status: finalStatus, error: error.message }, channelId);
|
|
437
|
+
broadcast({ type: 'error', content: `任务执行失败: ${error.message}${finalStatus === 'pending' ? ' (自动重试)' : ''}` }, channelId);
|
|
220
438
|
}
|
|
221
439
|
endTaskExecution();
|
|
222
440
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import * as fs from 'fs/promises';
|
|
14
14
|
import { CHANNELS_PATH, SESSION_CACHE_PATH, TASK_QUEUE_PATH, THEME_PATH, } from './server-types.js';
|
|
15
15
|
import { saveWindow as saveSessionWindow, loadWindow as loadSessionWindow } from '../bootstrap/session-window.js';
|
|
16
|
+
import { parentsSatisfied } from './task-deps.js';
|
|
16
17
|
// 写盘去重: 上次写盘内容, 用于跳过幂等调用
|
|
17
18
|
let lastChannelsJson = '';
|
|
18
19
|
// 写盘保护: 任何调用 saveChannels 后更新
|
|
@@ -179,6 +180,116 @@ export async function loadTaskQueue() {
|
|
|
179
180
|
export async function saveTaskQueue(tasks) {
|
|
180
181
|
await fs.writeFile(TASK_QUEUE_PATH, JSON.stringify(tasks, null, 2));
|
|
181
182
|
}
|
|
183
|
+
// ==================== 任务认领 CAS (2026-08-11, Hermes kanban 模式) ====================
|
|
184
|
+
// kanban_db.py: "WAL + BEGIN IMMEDIATE + compare-and-swap on tasks.status/claim_lock —
|
|
185
|
+
// 至多一个 worker 能认领任务, 输家看到 0 行受影响就退出, 无重试循环, 无分布式锁."
|
|
186
|
+
// Bolloon 对应: 任务队列是 JSON 文件, 用进程内互斥链 (withTaskQueueLock) 序列化
|
|
187
|
+
// load→CAS(status==='pending'→'running')→save, 输家返回 null/not-pending, 不重试.
|
|
188
|
+
let taskQueueLock = Promise.resolve();
|
|
189
|
+
function withTaskQueueLock(fn) {
|
|
190
|
+
const run = taskQueueLock.then(fn);
|
|
191
|
+
taskQueueLock = run.then(() => undefined, () => undefined);
|
|
192
|
+
return run;
|
|
193
|
+
}
|
|
194
|
+
/** 认领 TTL 默认 15min (BOLLOON_CLAIM_TTL_SECONDS 可调) — Hermes DEFAULT_CLAIM_TTL_SECONDS=15*60 */
|
|
195
|
+
export function claimTtlSeconds() {
|
|
196
|
+
const v = Number(process.env.BOLLOON_CLAIM_TTL_SECONDS || '');
|
|
197
|
+
return v > 0 ? v : 15 * 60;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* CAS 认领指定任务: 只有 status==='pending' 才能认领成功 (原子翻成 running).
|
|
201
|
+
* 认领点强制父依赖不变式 (Hermes kanban): 任一父未 completed/cancelled → 不认领, 返回 'parents-undone'.
|
|
202
|
+
* 认领写 claim_expires (TTL) — 崩溃/卡死后由 releaseStaleClaims 释放, 锁不泄漏.
|
|
203
|
+
*/
|
|
204
|
+
export async function claimTaskForExecution(taskId) {
|
|
205
|
+
if (isExecutingTask)
|
|
206
|
+
return 'busy';
|
|
207
|
+
return withTaskQueueLock(async () => {
|
|
208
|
+
if (isExecutingTask)
|
|
209
|
+
return 'busy';
|
|
210
|
+
const tasks = await loadTaskQueue();
|
|
211
|
+
const t = tasks.find((x) => x.id === taskId);
|
|
212
|
+
if (!t || t.status !== 'pending')
|
|
213
|
+
return 'not-pending';
|
|
214
|
+
// 父依赖不变式: 父未完成 → 拒绝认领 (依赖悬空时任务不可执行)
|
|
215
|
+
if (!parentsSatisfied(t, tasks))
|
|
216
|
+
return 'parents-undone';
|
|
217
|
+
t.status = 'running';
|
|
218
|
+
t.claimExpires = Date.now() + claimTtlSeconds() * 1000;
|
|
219
|
+
t.updatedAt = new Date().toISOString();
|
|
220
|
+
await saveTaskQueue(tasks);
|
|
221
|
+
isExecutingTask = true;
|
|
222
|
+
executionTaskId = taskId;
|
|
223
|
+
return 'claimed';
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* CAS 认领下一个 pending 任务; 无任务/已被认领 → null (输家不重试).
|
|
228
|
+
* 只认领父依赖满足的 pending — 父未完成的任务跳过 (等父 done 后可被认领), 不阻塞队列.
|
|
229
|
+
*/
|
|
230
|
+
export async function claimNextPendingTask() {
|
|
231
|
+
if (isExecutingTask)
|
|
232
|
+
return null;
|
|
233
|
+
return withTaskQueueLock(async () => {
|
|
234
|
+
if (isExecutingTask)
|
|
235
|
+
return null;
|
|
236
|
+
const tasks = await loadTaskQueue();
|
|
237
|
+
const t = tasks.find((x) => x.status === 'pending' && parentsSatisfied(x, tasks));
|
|
238
|
+
if (!t)
|
|
239
|
+
return null;
|
|
240
|
+
t.status = 'running';
|
|
241
|
+
t.claimExpires = Date.now() + claimTtlSeconds() * 1000;
|
|
242
|
+
t.updatedAt = new Date().toISOString();
|
|
243
|
+
await saveTaskQueue(tasks);
|
|
244
|
+
isExecutingTask = true;
|
|
245
|
+
executionTaskId = t.id;
|
|
246
|
+
return t;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* 心跳续期 (Hermes kanban_heartbeat): 执行中有实质进展时调用, 把 claim_expires 推到 now+TTL.
|
|
251
|
+
* CAS: 只续期当前执行中的任务 (executionTaskId 匹配 + status==='running').
|
|
252
|
+
*/
|
|
253
|
+
export async function heartbeatTaskExecution(taskId) {
|
|
254
|
+
return withTaskQueueLock(async () => {
|
|
255
|
+
if (executionTaskId !== taskId)
|
|
256
|
+
return false;
|
|
257
|
+
const tasks = await loadTaskQueue();
|
|
258
|
+
const t = tasks.find((x) => x.id === taskId);
|
|
259
|
+
if (!t || t.status !== 'running')
|
|
260
|
+
return false;
|
|
261
|
+
t.claimExpires = Date.now() + claimTtlSeconds() * 1000;
|
|
262
|
+
t.updatedAt = new Date().toISOString();
|
|
263
|
+
await saveTaskQueue(tasks);
|
|
264
|
+
return true;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* 释放过期认领 (Hermes release_stale_claims): 扫所有 running 且 claim_expires 已过期的任务
|
|
269
|
+
* → 释放 in-memory 锁 + 任务回 pending (可重新认领执行). 防 executeTask 卡死/崩溃后锁泄漏.
|
|
270
|
+
* 返回释放数量. 安全可频繁调用.
|
|
271
|
+
*/
|
|
272
|
+
export async function releaseStaleClaims() {
|
|
273
|
+
return withTaskQueueLock(async () => {
|
|
274
|
+
const now = Date.now();
|
|
275
|
+
const tasks = await loadTaskQueue();
|
|
276
|
+
const stale = tasks.filter((t) => t.status === 'running' && t.claimExpires !== undefined && t.claimExpires < now);
|
|
277
|
+
if (stale.length === 0)
|
|
278
|
+
return 0;
|
|
279
|
+
for (const t of stale) {
|
|
280
|
+
t.status = 'pending';
|
|
281
|
+
t.claimExpires = undefined;
|
|
282
|
+
t.updatedAt = new Date().toISOString();
|
|
283
|
+
}
|
|
284
|
+
await saveTaskQueue(tasks);
|
|
285
|
+
// 当前执行锁指向过期任务 → 一并释放 (卡死的 executeTask 其锁也失效)
|
|
286
|
+
if (executionTaskId && stale.some((t) => t.id === executionTaskId)) {
|
|
287
|
+
isExecutingTask = false;
|
|
288
|
+
executionTaskId = null;
|
|
289
|
+
}
|
|
290
|
+
return stale.length;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
182
293
|
/** 执行 task 的 helper 标记 (server.ts 内部 still 持有 lock) */
|
|
183
294
|
export function startTaskExecution(taskId) {
|
|
184
295
|
if (isExecutingTask)
|
package/dist/web/server.js
CHANGED
|
@@ -2397,19 +2397,19 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
2397
2397
|
.map((p) => `- ${p.name || p.publicKey.slice(0, 8)} (pk=${p.publicKey.slice(0, 12)}…): 渠道[${p.channels.map((c) => c.name).join(', ') || '无'}]`)
|
|
2398
2398
|
.join('\n');
|
|
2399
2399
|
const goalDesc = ctx.goal ? `当前目标: ${ctx.goal.description} (已发起 ${ctx.goal.initiationsUsed}/${ctx.goal.maxInitiations}, 有效回复 ${ctx.goal.effectfulReplies}/${ctx.goal.effectThreshold})` : '当前无明确目标';
|
|
2400
|
-
const prompt = `你是智能体「${ctx.self.name || '本地智能体'}」。你通过 P2P 网络认识以下其他智能体:
|
|
2401
|
-
${peerLines}
|
|
2402
|
-
|
|
2403
|
-
${goalDesc}
|
|
2404
|
-
|
|
2405
|
-
规则:
|
|
2406
|
-
1. 社交是为了达成上述目标, 不是闲聊。只在你有真正有价值的信息要分享/询问、且能推进目标时才主动发起。
|
|
2407
|
-
2. 不要重复最近已经聊过的话题, 不要每条心跳都发消息, 保持克制。
|
|
2408
|
-
3. 如果目标已经通过已有交流达成 (或你认为无需再聊), 输出 {"initiate": false, "goalAchieved": true}。
|
|
2409
|
-
4. 如果决定发起, 选一个最合适的目标渠道 (用对方渠道的真实 id)。
|
|
2410
|
-
|
|
2411
|
-
现在是否要主动联系其中某个智能体? 只输出一个 JSON 对象, 不要任何其他文字:
|
|
2412
|
-
{"initiate": true 或 false, "goalAchieved": true 或 false, "targetPeerPublicKey": "对方 pk", "targetChannelId": "对方渠道 id", "message": "你要说的话"}
|
|
2400
|
+
const prompt = `你是智能体「${ctx.self.name || '本地智能体'}」。你通过 P2P 网络认识以下其他智能体:
|
|
2401
|
+
${peerLines}
|
|
2402
|
+
|
|
2403
|
+
${goalDesc}
|
|
2404
|
+
|
|
2405
|
+
规则:
|
|
2406
|
+
1. 社交是为了达成上述目标, 不是闲聊。只在你有真正有价值的信息要分享/询问、且能推进目标时才主动发起。
|
|
2407
|
+
2. 不要重复最近已经聊过的话题, 不要每条心跳都发消息, 保持克制。
|
|
2408
|
+
3. 如果目标已经通过已有交流达成 (或你认为无需再聊), 输出 {"initiate": false, "goalAchieved": true}。
|
|
2409
|
+
4. 如果决定发起, 选一个最合适的目标渠道 (用对方渠道的真实 id)。
|
|
2410
|
+
|
|
2411
|
+
现在是否要主动联系其中某个智能体? 只输出一个 JSON 对象, 不要任何其他文字:
|
|
2412
|
+
{"initiate": true 或 false, "goalAchieved": true 或 false, "targetPeerPublicKey": "对方 pk", "targetChannelId": "对方渠道 id", "message": "你要说的话"}
|
|
2413
2413
|
若不想发起, 输出 {"initiate": false}。`;
|
|
2414
2414
|
const raw = await agent.promptStream(prompt, () => { }, undefined, local.id);
|
|
2415
2415
|
const m = raw.match(/\{[\s\S]*\}/);
|
|
@@ -7226,20 +7226,49 @@ ${goalDesc}
|
|
|
7226
7226
|
console.log('服务器已监听');
|
|
7227
7227
|
// 安装 chat bus -> SSE 桥 (供前端 inbox UI 实时刷新)
|
|
7228
7228
|
void installChatBusHook();
|
|
7229
|
-
// 2026-
|
|
7230
|
-
//
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7229
|
+
// 2026-08-11 (Hermes release_stale_claims): 任务认领 TTL 过期释放 —
|
|
7230
|
+
// 60s 周期清扫 + 启动即清一次 (重启后进程内锁已丢, 磁盘上 running+过期的任务要回收)
|
|
7231
|
+
void (async () => {
|
|
7232
|
+
const { releaseStaleClaims } = await import('./server-storage.js');
|
|
7233
|
+
const n0 = await releaseStaleClaims();
|
|
7234
|
+
if (n0 > 0)
|
|
7235
|
+
console.warn(`[task-claim] 启动回收 ${n0} 个过期认领任务`);
|
|
7236
|
+
setInterval(() => {
|
|
7237
|
+
void releaseStaleClaims().then((n) => {
|
|
7238
|
+
if (n > 0)
|
|
7239
|
+
console.warn(`[task-claim] 定时回收 ${n} 个过期认领任务`);
|
|
7240
|
+
});
|
|
7241
|
+
}, 60_000);
|
|
7242
|
+
})();
|
|
7243
|
+
// 2026-08-11 (Hermes delivery_ledger + channel_directory 模式): SSE 投递账本 —
|
|
7244
|
+
// ping/广播写账本 (attempting→delivered/failed), 同类错误窗口内只 warn 一次 (防刷屏)
|
|
7245
|
+
void (async () => {
|
|
7246
|
+
const { DeliveryLedger } = await import('./delivery-ledger.js');
|
|
7247
|
+
const deliveryLedger = new DeliveryLedger();
|
|
7248
|
+
// 2026-06-16: ping 改为 data: {"type":"ping"} — 之前是 SSE 注释格式 (: ping\n\n),
|
|
7249
|
+
// 浏览器 EventSource 不触发 onmessage, 客户端 60s 阈值 (现已 30s) 误判死链.
|
|
7250
|
+
// 改后前端 onmessage 收到 ping 就重置 lastEventTime, 真死链才 30s 后重建.
|
|
7251
|
+
setInterval(() => {
|
|
7252
|
+
for (const client of sseClients) {
|
|
7253
|
+
const clientId = String(client.channelId ?? 'anon');
|
|
7254
|
+
deliveryLedger.markAttempting(clientId);
|
|
7255
|
+
try {
|
|
7256
|
+
client.res.write('data: {"type":"ping"}\n\n');
|
|
7257
|
+
deliveryLedger.markDelivered(clientId);
|
|
7258
|
+
}
|
|
7259
|
+
catch (err) {
|
|
7260
|
+
// socket 已断, 跳过 — client 端 onerror 会触发重连; 同类错误节流后只 warn 一次
|
|
7261
|
+
const shouldWarn = deliveryLedger.markFailed(clientId, err);
|
|
7262
|
+
if (shouldWarn) {
|
|
7263
|
+
console.warn(`[SSE ping] write 失败 (${deliveryLedger.get(clientId)?.consecutiveFailures} 连败), 跳过该客户端:`, err.message);
|
|
7264
|
+
}
|
|
7265
|
+
}
|
|
7240
7266
|
}
|
|
7241
|
-
|
|
7242
|
-
|
|
7267
|
+
const s = deliveryLedger.stats();
|
|
7268
|
+
if (s.dead > 0)
|
|
7269
|
+
console.warn(`[SSE ledger] ${s.dead} 个客户端判定死亡 (待重连), 存活 ${s.delivered}`);
|
|
7270
|
+
}, 30000);
|
|
7271
|
+
})();
|
|
7243
7272
|
// 2026-06-16: 全局捕获 socket error 事件, 避免未处理 EPIPE/ETIMEDOUT 让进程崩
|
|
7244
7273
|
currentServer.on('clientError', (err, socket) => {
|
|
7245
7274
|
console.warn('[server] clientError:', err.code, err.message);
|