@lawrenceliang-btc/atel-sdk 1.1.5 → 1.1.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # ATEL SDK
2
2
 
3
- **Agent Trust & Exchange Layer** — A TypeScript protocol SDK for building trustworthy, auditable multi-agent systems.
3
+ **Agent Trust & Exchange Layer** — A protocol SDK and CLI for trustworthy, auditable multi-agent collaboration.
4
4
 
5
5
  ## Core Capabilities
6
6
 
@@ -12,16 +12,15 @@ ATEL provides the cryptographic primitives and protocol building blocks that ena
12
12
  - **✅ Proof Generation** — Merkle-tree proof bundles with multi-check verification
13
13
  - **⚓ On-Chain Anchoring** — Multi-chain proof anchoring (Solana/Base/BSC)
14
14
  - **📊 Trust Scoring** — Local trust computation based on execution history
15
- - **🤖 CoT Audit** — Chain-of-thought reasoning verification with local LLM
15
+ - **🔔 Notification & Callback Runtime** — Local notify, callback, inbox, and recovery flow
16
16
  - **👥 P2P Access Control** — Relationship-based friend system with temporary sessions
17
17
 
18
18
  ## Key Features
19
19
 
20
- ### Zero-Config Deployment
21
- - No external dependencies (no Ollama, no Docker)
22
- - Auto-downloads audit model on first run (~400MB)
23
- - Pure Node.js with node-llama-cpp
24
- - Cross-platform (Linux/macOS/Windows)
20
+ ### Runtime Model
21
+ - ATEL handles DID identity, relay, inbox, callback, notification, and paid order state
22
+ - OpenClaw or your own runtime handles reasoning and tool use
23
+ - Cross-platform CLI (Linux/macOS/Windows)
25
24
 
26
25
  ### P2P Friend System
27
26
  - Relationship-based access control (friends-only mode)
@@ -35,14 +34,14 @@ ATEL provides the cryptographic primitives and protocol building blocks that ena
35
34
  - Merkle-tree proof generation
36
35
  - On-chain anchoring (Solana/Base/BSC)
37
36
  - Local trust score computation
38
- - CoT reasoning audit with local LLM
37
+ - Callback-driven execution and recovery
39
38
 
40
39
  ### Developer Experience
41
40
  - Comprehensive CLI with detailed help
42
41
  - Unified output format (human/json/quiet)
43
42
  - Status commands for system overview
44
43
  - Confirmation prompts for destructive operations
45
- - 13 composable modules
44
+ - Skill-first onboarding path
46
45
 
47
46
  ## Quick Start
48
47
 
@@ -60,47 +59,52 @@ atel register "My Agent" "assistant,research"
60
59
  atel start 3100
61
60
  ```
62
61
 
63
- ### First Run (Auto-Downloads Model)
62
+ ### Recommended Runtime
63
+
64
+ ATEL is not a built-in general-purpose LLM executor. The recommended setup is:
65
+
66
+ - OpenClaw handles agent reasoning and tool execution
67
+ - `atel start` handles endpoint, relay, callback, inbox, and notifications
68
+ - the provided `SKILL.md` handles setup and runtime conventions
69
+
70
+ For OpenClaw, enable `sessions_spawn` in Gateway and start the ATEL runtime:
64
71
 
65
72
  ```bash
73
+ openclaw gateway restart
66
74
  atel start 3100
67
- # 📦 Downloading model (first time only, ~400MB)...
68
- # Progress: 100% (408.9/408.9 MB)
69
- # ✅ Model ready
70
- # 🚀 Agent started on port 3100
71
75
  ```
72
76
 
77
+ For custom runtimes, point `ATEL_EXECUTOR_URL` at your own service.
78
+
73
79
  ## Architecture
74
80
 
75
- ATEL is organized into 13 composable modules:
81
+ ATEL is organized into protocol and runtime layers:
76
82
 
77
83
  ```
78
84
  ┌──────────────────────────────────────────────────────────────┐
79
- │ ATEL SDK
85
+ │ ATEL CLI / SDK
80
86
  ├──────────┬──────────┬──────────┬──────────┬─────────────────┤
81
- │ Identity │ Schema │ Policy │ Gateway │ Trace │
87
+ │ Identity │ Registry │ Policy │ Relay │ Trace │
82
88
  ├──────────┴──────────┴──────────┴──────────┴─────────────────┤
83
- │ Proof │ ScoreGraphTrustManagerRollbackAnchor
89
+ │ Proof │ NotifyCallbackTradeAnchorTrust/Score
84
90
  ├───────────────────────────────┬──────────────────────────────┤
85
- Orchestrator │ Trust Score Service
91
+ Local Runtime State │ External Agent Runtime
86
92
  └───────────────────────────────┴──────────────────────────────┘
87
93
  ```
88
94
 
89
95
  | Module | Description |
90
96
  |--------|-------------|
91
97
  | **Identity** | Ed25519 keypairs, DID creation, signing & verification |
92
- | **Schema** | Task and capability schemas, validation, matching |
93
- | **Policy** | Consent tokens, policy engine with call tracking |
94
- | **Gateway** | Tool invocation gateway with policy enforcement |
98
+ | **Registry** | Agent registration, discovery, metadata |
99
+ | **Policy** | Access control and task acceptance policy |
100
+ | **Relay** | Message delivery, inbox, connectivity fallback |
95
101
  | **Trace** | Append-only, hash-chained execution log |
96
102
  | **Proof** | Merkle-tree proof bundles with verification |
97
- | **Score** | Local trust-score computation |
98
- | **Graph** | Multi-dimensional trust graph |
99
- | **TrustManager** | Unified score + graph API |
100
- | **Rollback** | Compensation and rollback execution |
103
+ | **Notify** | Local user notifications and target fan-out |
104
+ | **Callback** | Runtime callback, recovery, and dedupe handling |
105
+ | **Trade** | Paid order flow, milestone state, settlement hooks |
101
106
  | **Anchor** | Multi-chain proof anchoring |
102
- | **Orchestrator** | High-level task delegation API |
103
- | **Service** | HTTP API for trust queries |
107
+ | **Trust/Score** | Local trust-score computation and risk checks |
104
108
 
105
109
  ## CLI Commands
106
110
 
@@ -140,10 +144,11 @@ atel friend add @alice --notes "Met at conference"
140
144
  atel temp-session allow @bob --duration 120
141
145
  ```
142
146
 
143
- ### Task Execution
147
+ ### P2P Collaboration
144
148
  ```bash
145
- atel task <target> <json> # Delegate task to agent
146
- atel result <taskId> <json> # Submit execution result
149
+ atel task <target> <json> # Direct P2P task
150
+ atel result <taskId> <json> # Submit execution result
151
+ atel inbox # Inspect pending direct tasks/messages
147
152
  ```
148
153
 
149
154
  ### Trust & Verification
@@ -155,12 +160,14 @@ atel verify-proof <tx> <root> # Verify on-chain proof
155
160
 
156
161
  ### Registry & Trading
157
162
  ```bash
158
- atel register [name] [caps] # Register on public registry
159
- atel search <capability> # Search for agents
160
- atel order <did> <cap> <price> # Create trade order
161
- atel accept <orderId> # Accept order
162
- atel complete <orderId> # Mark complete
163
- atel confirm <orderId> # Confirm and settle
163
+ atel register [name] [caps] # Register on public registry
164
+ atel search <capability> # Search for agents
165
+ atel order <did> <cap> <price> # Create paid order
166
+ atel accept <orderId> # Accept order
167
+ atel milestone-status <orderId> # Inspect current plan/progress
168
+ atel milestone-feedback <orderId> --approve # Approve plan
169
+ atel milestone-submit <orderId> <index> --result # Submit milestone result
170
+ atel milestone-verify <orderId> <index> --pass # Verify submitted milestone
164
171
  ```
165
172
 
166
173
  ## API Examples
package/bin/atel.mjs CHANGED
@@ -89,6 +89,8 @@ const PENDING_FILE = resolve(ATEL_DIR, 'pending-tasks.json');
89
89
  const RESULT_PUSH_QUEUE_FILE = resolve(ATEL_DIR, 'pending-result-pushes.json');
90
90
  const NOTIFY_TARGETS_FILE = resolve(ATEL_DIR, 'notify-targets.json');
91
91
  const TRADE_TRACK_FILE = resolve(ATEL_DIR, 'tracked-orders.json');
92
+ const P2P_STATUS_FILE = resolve(ATEL_DIR, 'p2p-task-status.jsonl');
93
+ const PENDING_AGENT_CALLBACKS_FILE = resolve(ATEL_DIR, 'pending-agent-callbacks.json');
92
94
  const KEYS_DIR = resolve(ATEL_DIR, 'keys');
93
95
  const ANCHOR_FILE = resolve(KEYS_DIR, 'anchor.json');
94
96
 
@@ -244,33 +246,155 @@ async function pushTradeNotification(eventType, payload, body) {
244
246
  try { saveNotifyTargets(targets); } catch {}
245
247
  }
246
248
 
249
+ function appendP2PTaskStatus(entry) {
250
+ if (!entry?.taskId || !entry?.status) return;
251
+ ensureDir();
252
+ appendFileSync(P2P_STATUS_FILE, `${JSON.stringify({ updatedAt: new Date().toISOString(), ...entry })}\n`);
253
+ }
254
+
255
+ async function pushP2PNotification(eventType, payload = {}) {
256
+ const targets = loadNotifyTargets();
257
+ const enabled = (targets.targets || []).filter(t => t.enabled !== false);
258
+ if (enabled.length === 0) return;
259
+
260
+ const templates = {
261
+ 'p2p_task_sent': (p) => `📤 P2P任务已发送\n任务: ${p.taskId || '?'}\n目标: ${p.peerDid || '?'}`,
262
+ 'p2p_task_received': (p) => `📩 收到新的P2P任务\n任务: ${p.taskId || '?'}\n来自: ${p.peerDid || '?'}`,
263
+ 'p2p_task_started': (p) => `▶️ P2P任务开始处理\n任务: ${p.taskId || '?'}\n来自: ${p.peerDid || '?'}`,
264
+ 'p2p_result_submitted': (p) => `📨 P2P结果已发回对方\n任务: ${p.taskId || '?'}\n目标: ${p.peerDid || '?'}`,
265
+ 'p2p_result_received': (p) => `✅ P2P任务已完成\n任务: ${p.taskId || '?'}\n结果: ${String(p.result || '').slice(0, 80) || '已返回'}`,
266
+ 'p2p_task_failed': (p) => `❌ P2P任务失败\n任务: ${p.taskId || '?'}\n原因: ${p.reason || '未知错误'}`,
267
+ };
268
+ const tmpl = templates[eventType];
269
+ if (!tmpl) return;
270
+ const message = tmpl(payload);
271
+
272
+ for (const target of enabled) {
273
+ try {
274
+ if (target.channel === 'telegram' && target.botToken) {
275
+ await fetch(`https://api.telegram.org/bot${target.botToken}/sendMessage`, {
276
+ method: 'POST',
277
+ headers: { 'Content-Type': 'application/json' },
278
+ body: JSON.stringify({ chat_id: target.target, text: message, parse_mode: 'HTML' }),
279
+ signal: AbortSignal.timeout(5000),
280
+ }).catch(() => {});
281
+ } else if (target.channel === 'gateway') {
282
+ const gw = discoverGateway();
283
+ if (gw?.url && gw?.token) {
284
+ await fetch(`${gw.url}/tools/invoke`, {
285
+ method: 'POST',
286
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${gw.token}` },
287
+ body: JSON.stringify({ tool: 'message', args: { action: 'send', message, target: target.target } }),
288
+ signal: AbortSignal.timeout(5000),
289
+ }).catch(() => {});
290
+ }
291
+ }
292
+ target.lastUsedAt = new Date().toISOString();
293
+ } catch {}
294
+ }
295
+ try { saveNotifyTargets(targets); } catch {}
296
+ }
297
+
298
+ function loadPendingAgentCallbacksPersisted() {
299
+ if (!existsSync(PENDING_AGENT_CALLBACKS_FILE)) return {};
300
+ try {
301
+ const data = JSON.parse(readFileSync(PENDING_AGENT_CALLBACKS_FILE, 'utf-8'));
302
+ return data && typeof data === 'object' ? data : {};
303
+ } catch {
304
+ return {};
305
+ }
306
+ }
307
+
308
+ function savePendingAgentCallbacksPersisted(data) {
309
+ ensureDir();
310
+ writeFileSync(PENDING_AGENT_CALLBACKS_FILE, JSON.stringify(data, null, 2));
311
+ }
312
+
313
+ function persistPendingAgentCallback(dedupeKey, data) {
314
+ if (!dedupeKey || !data) return;
315
+ const persisted = loadPendingAgentCallbacksPersisted();
316
+ persisted[dedupeKey] = {
317
+ ...data,
318
+ updatedAt: new Date().toISOString(),
319
+ };
320
+ savePendingAgentCallbacksPersisted(persisted);
321
+ }
322
+
323
+ function markPersistedPendingAgentCallbackCompleted(dedupeKey, meta = {}) {
324
+ if (!dedupeKey) return;
325
+ const persisted = loadPendingAgentCallbacksPersisted();
326
+ const existing = persisted[dedupeKey];
327
+ persisted[dedupeKey] = {
328
+ ...(existing || {}),
329
+ ...meta,
330
+ terminal: true,
331
+ completedAt: new Date().toISOString(),
332
+ updatedAt: new Date().toISOString(),
333
+ };
334
+ savePendingAgentCallbacksPersisted(persisted);
335
+ }
336
+
337
+ function clearPersistedPendingAgentCallback(dedupeKey) {
338
+ if (!dedupeKey) return;
339
+ const persisted = loadPendingAgentCallbacksPersisted();
340
+ if (!persisted[dedupeKey]) return;
341
+ delete persisted[dedupeKey];
342
+ savePendingAgentCallbacksPersisted(persisted);
343
+ }
344
+
247
345
  async function executeRecommendedActionDirect(eventType, action, cwd, dedupeKey) {
248
346
  const command = Array.isArray(action?.command) ? action.command.filter(Boolean) : [];
249
347
  if (command.length === 0) {
250
348
  return { ok: false, skipped: true, reason: 'empty_command' };
251
349
  }
252
350
 
253
- // Idempotency guard: if a milestone review action races with reconciliation and the
254
- // milestone has already advanced out of `submitted`, skip the duplicate verify call.
255
- if (command[0] === 'atel' && command[1] === 'milestone-verify' && command.length >= 4) {
351
+ // Idempotency guard: short-circuit duplicate milestone plan/submit/verify actions
352
+ // if the order or milestone has already advanced past the required state.
353
+ if (command[0] === 'atel' && (command[1] === 'milestone-feedback' || command[1] === 'milestone-verify' || command[1] === 'milestone-submit') && command.length >= 3) {
256
354
  const orderId = command[2];
257
- const index = Number.parseInt(String(command[3]), 10);
258
- if (orderId && Number.isFinite(index)) {
355
+ const needsMilestoneIndex = command[1] === 'milestone-verify' || command[1] === 'milestone-submit';
356
+ const index = needsMilestoneIndex ? Number.parseInt(String(command[3]), 10) : null;
357
+ if (orderId && (!needsMilestoneIndex || Number.isFinite(index))) {
259
358
  try {
260
359
  const resp = await fetch(`${PLATFORM_URL}/trade/v1/order/${orderId}/milestones`, { signal: AbortSignal.timeout(10000) });
261
360
  if (resp.ok) {
262
361
  const state = await resp.json();
263
- const milestone = Array.isArray(state?.milestones) ? state.milestones.find((m) => m.index === index) : null;
264
- if (milestone && milestone.status !== 'submitted') {
362
+ if (command[1] === 'milestone-feedback' && command.includes('--approve') && state?.orderStatus && state.orderStatus !== 'milestone_review') {
363
+ log({
364
+ event: 'recommended_action_direct_skip',
365
+ eventType,
366
+ dedupeKey,
367
+ action: action.action,
368
+ command,
369
+ reason: `order_status_${state.orderStatus}`,
370
+ });
371
+ return { ok: true, skipped: true, reason: `order_status_${state.orderStatus}` };
372
+ }
373
+ if (command[1] === 'milestone-submit' && state?.orderStatus && state.orderStatus !== 'executing') {
265
374
  log({
266
375
  event: 'recommended_action_direct_skip',
267
376
  eventType,
268
377
  dedupeKey,
269
378
  action: action.action,
270
379
  command,
271
- reason: `milestone_status_${milestone.status}`,
380
+ reason: `order_status_${state.orderStatus}`,
272
381
  });
273
- return { ok: true, skipped: true, reason: `milestone_status_${milestone.status}` };
382
+ return { ok: true, skipped: true, reason: `order_status_${state.orderStatus}` };
383
+ }
384
+ const milestone = needsMilestoneIndex && Array.isArray(state?.milestones) ? state.milestones.find((m) => m.index === index) : null;
385
+ if (needsMilestoneIndex && milestone) {
386
+ const expectedStatus = command[1] === 'milestone-verify' ? 'submitted' : 'pending';
387
+ if (milestone.status !== expectedStatus) {
388
+ log({
389
+ event: 'recommended_action_direct_skip',
390
+ eventType,
391
+ dedupeKey,
392
+ action: action.action,
393
+ command,
394
+ reason: `milestone_status_${milestone.status}`,
395
+ });
396
+ return { ok: true, skipped: true, reason: `milestone_status_${milestone.status}` };
397
+ }
274
398
  }
275
399
  }
276
400
  } catch (e) {
@@ -2392,6 +2516,63 @@ async function cmdStart(port) {
2392
2516
  const dedupeKey = body.dedupeKey;
2393
2517
  const pending = dedupeKey ? pendingAgentCallbacks.get(dedupeKey) : null;
2394
2518
  if (!pending) {
2519
+ const persisted = dedupeKey ? loadPendingAgentCallbacksPersisted()[dedupeKey] : null;
2520
+ if (persisted?.terminal) {
2521
+ log({
2522
+ event: 'callback_late_skip',
2523
+ dedupeKey,
2524
+ eventType: persisted.eventType || body?.eventType || 'unknown',
2525
+ callback_source: persisted.source || 'unknown',
2526
+ });
2527
+ res.json({ status: 'ok', skipped: true, reason: 'late_callback_after_completion' });
2528
+ return;
2529
+ }
2530
+ log({
2531
+ event: 'callback_404',
2532
+ callback_404_type: 'unknown_dedupeKey',
2533
+ dedupeKey,
2534
+ callback_404_recovered: !!persisted,
2535
+ callback_source: persisted?.source || 'unknown',
2536
+ eventType: persisted?.eventType || body?.eventType || 'unknown',
2537
+ });
2538
+ if (persisted) {
2539
+ const callbackAction = buildAgentCallbackAction(persisted.eventType, persisted.payload || {}, body);
2540
+ if (callbackAction.ok && callbackAction.action?.type === 'local_result') {
2541
+ try {
2542
+ const localResp = await fetch(`http://127.0.0.1:${p}/atel/v1/result`, {
2543
+ method: 'POST',
2544
+ headers: { 'Content-Type': 'application/json' },
2545
+ body: JSON.stringify({
2546
+ taskId: callbackAction.action.taskId,
2547
+ result: callbackAction.action.result,
2548
+ success: true,
2549
+ }),
2550
+ signal: AbortSignal.timeout(15000),
2551
+ });
2552
+ const localBody = await localResp.json().catch(() => ({}));
2553
+ if (localResp.ok) {
2554
+ clearPersistedPendingAgentCallback(dedupeKey);
2555
+ res.json({ status: 'ok', recovered: true });
2556
+ return;
2557
+ }
2558
+ res.status(500).json({ error: localBody.error || 'local_result_callback_failed', recovered: false });
2559
+ return;
2560
+ } catch (e) {
2561
+ res.status(500).json({ error: e.message || 'local_result_callback_failed', recovered: false });
2562
+ return;
2563
+ }
2564
+ }
2565
+ if (callbackAction.ok) {
2566
+ const execResult = await executeRecommendedActionDirect(persisted.eventType, callbackAction.action, persisted.cwd || process.cwd(), dedupeKey);
2567
+ if (execResult.ok) {
2568
+ clearPersistedPendingAgentCallback(dedupeKey);
2569
+ res.json({ status: 'ok', recovered: true });
2570
+ return;
2571
+ }
2572
+ res.status(500).json({ error: execResult.error || 'callback_action_failed', recovered: false });
2573
+ return;
2574
+ }
2575
+ }
2395
2576
  res.status(404).json({ error: 'Unknown dedupeKey' });
2396
2577
  return;
2397
2578
  }
@@ -2407,6 +2588,7 @@ async function cmdStart(port) {
2407
2588
 
2408
2589
  if (body.status === 'failed') {
2409
2590
  pendingAgentCallbacks.delete(dedupeKey);
2591
+ clearPersistedPendingAgentCallback(dedupeKey);
2410
2592
  pending.resolve({ ok: false, body, error: body.error || 'agent_reported_failed' });
2411
2593
  res.json({ status: 'ok' });
2412
2594
  return;
@@ -2415,12 +2597,14 @@ async function cmdStart(port) {
2415
2597
  const callbackAction = buildAgentCallbackAction(pending.eventType, pending.payload || {}, body);
2416
2598
  if (callbackAction.skipped) {
2417
2599
  pendingAgentCallbacks.delete(dedupeKey);
2600
+ markPersistedPendingAgentCallbackCompleted(dedupeKey, { eventType: pending.eventType, payload: pending.payload, cwd: pending.cwd, source: dedupeKey?.startsWith('reconcile:') ? 'reconcile' : 'main', skipped: true, reason: callbackAction.reason });
2418
2601
  pending.resolve({ ok: true, skipped: true, body, reason: callbackAction.reason });
2419
2602
  res.json({ status: 'ok', skipped: true });
2420
2603
  return;
2421
2604
  }
2422
2605
  if (!callbackAction.ok) {
2423
2606
  pendingAgentCallbacks.delete(dedupeKey);
2607
+ clearPersistedPendingAgentCallback(dedupeKey);
2424
2608
  pending.resolve({ ok: false, body, error: callbackAction.error || 'invalid_callback_payload' });
2425
2609
  res.status(400).json({ error: callbackAction.error || 'invalid_callback_payload' });
2426
2610
  return;
@@ -2440,6 +2624,11 @@ async function cmdStart(port) {
2440
2624
  });
2441
2625
  const localBody = await localResp.json().catch(() => ({}));
2442
2626
  pendingAgentCallbacks.delete(dedupeKey);
2627
+ if (localResp.ok) {
2628
+ markPersistedPendingAgentCallbackCompleted(dedupeKey, { eventType: pending.eventType, payload: pending.payload, cwd: pending.cwd, source: dedupeKey?.startsWith('reconcile:') ? 'reconcile' : 'main', action: callbackAction.action, localBody });
2629
+ } else {
2630
+ clearPersistedPendingAgentCallback(dedupeKey);
2631
+ }
2443
2632
  pending.resolve({ ok: localResp.ok, body, action: callbackAction.action, localBody });
2444
2633
  if (!localResp.ok) {
2445
2634
  res.status(500).json({ error: localBody.error || 'local_result_callback_failed' });
@@ -2449,6 +2638,7 @@ async function cmdStart(port) {
2449
2638
  return;
2450
2639
  } catch (e) {
2451
2640
  pendingAgentCallbacks.delete(dedupeKey);
2641
+ clearPersistedPendingAgentCallback(dedupeKey);
2452
2642
  pending.resolve({ ok: false, body, action: callbackAction.action, error: e.message });
2453
2643
  res.status(500).json({ error: e.message || 'local_result_callback_failed' });
2454
2644
  return;
@@ -2457,6 +2647,11 @@ async function cmdStart(port) {
2457
2647
 
2458
2648
  const execResult = await executeRecommendedActionDirect(pending.eventType, callbackAction.action, pending.cwd || process.cwd(), dedupeKey);
2459
2649
  pendingAgentCallbacks.delete(dedupeKey);
2650
+ if (execResult.ok) {
2651
+ markPersistedPendingAgentCallbackCompleted(dedupeKey, { eventType: pending.eventType, payload: pending.payload, cwd: pending.cwd, source: dedupeKey?.startsWith('reconcile:') ? 'reconcile' : 'main', action: callbackAction.action });
2652
+ } else {
2653
+ clearPersistedPendingAgentCallback(dedupeKey);
2654
+ }
2460
2655
  pending.resolve({ ok: execResult.ok, body, action: callbackAction.action, execResult });
2461
2656
  if (!execResult.ok) {
2462
2657
  res.status(500).json({ error: execResult.error || 'callback_action_failed' });
@@ -2554,6 +2749,7 @@ ${callbackFailed}
2554
2749
  return await new Promise(async (resolve) => {
2555
2750
  const timer = setTimeout(() => {
2556
2751
  pendingAgentCallbacks.delete(dedupeKey);
2752
+ persistPendingAgentCallback(dedupeKey, { eventType, payload, cwd, source: dedupeKey.startsWith('reconcile:') ? 'reconcile' : 'main', timeout: true });
2557
2753
  resolve({ ok: false, error: 'agent_callback_timeout' });
2558
2754
  }, timeoutMs);
2559
2755
 
@@ -2567,6 +2763,12 @@ ${callbackFailed}
2567
2763
  resolve(result);
2568
2764
  },
2569
2765
  });
2766
+ persistPendingAgentCallback(dedupeKey, {
2767
+ eventType,
2768
+ payload,
2769
+ cwd,
2770
+ source: dedupeKey.startsWith('reconcile:') ? 'reconcile' : 'main',
2771
+ });
2570
2772
 
2571
2773
  try {
2572
2774
  const resp = await fetch(`${gw.url}/tools/invoke`, {
@@ -2586,6 +2788,7 @@ ${callbackFailed}
2586
2788
  });
2587
2789
  if (!resp.ok) {
2588
2790
  pendingAgentCallbacks.delete(dedupeKey);
2791
+ clearPersistedPendingAgentCallback(dedupeKey);
2589
2792
  clearTimeout(timer);
2590
2793
  resolve({ ok: false, error: `sessions_spawn_http_${resp.status}` });
2591
2794
  return;
@@ -2598,6 +2801,7 @@ ${callbackFailed}
2598
2801
  log({ event: 'agent_session_spawned', eventType, dedupeKey, childSessionKey });
2599
2802
  } catch (e) {
2600
2803
  pendingAgentCallbacks.delete(dedupeKey);
2804
+ clearPersistedPendingAgentCallback(dedupeKey);
2601
2805
  clearTimeout(timer);
2602
2806
  resolve({ ok: false, error: e.message });
2603
2807
  }
@@ -2958,7 +3162,24 @@ ${callbackFailed}
2958
3162
  // Result callback: POST /atel/v1/result (executor calls this when done)
2959
3163
  endpoint.app?.post?.('/atel/v1/result', async (req, res) => {
2960
3164
  const { taskId, result, success, trace: executorTrace } = req.body || {};
2961
- if (!taskId || !pendingTasks[taskId]) { res.status(404).json({ error: 'Unknown taskId' }); return; }
3165
+ if (!taskId || !pendingTasks[taskId]) {
3166
+ try {
3167
+ if (existsSync(P2P_STATUS_FILE)) {
3168
+ const lines = readFileSync(P2P_STATUS_FILE, 'utf-8').split('\n').filter(Boolean);
3169
+ for (let i = lines.length - 1; i >= 0; i--) {
3170
+ const entry = JSON.parse(lines[i]);
3171
+ if (entry?.taskId === taskId && entry?.status === 'result_received') {
3172
+ log({ event: 'callback_late_skip', taskId, callback_source: 'result' });
3173
+ res.json({ status: 'ok', skipped: true, reason: 'late_result_after_completion' });
3174
+ return;
3175
+ }
3176
+ }
3177
+ }
3178
+ } catch {}
3179
+ log({ event: 'callback_404', callback_404_type: 'unknown_taskId', taskId, callback_404_recovered: false, callback_source: 'result' });
3180
+ res.status(404).json({ error: 'Unknown taskId' });
3181
+ return;
3182
+ }
2962
3183
 
2963
3184
  // Milestone auto-execution removed. Agent handles results via its own AI.
2964
3185
  const task = pendingTasks[taskId];
@@ -3242,6 +3463,8 @@ ${callbackFailed}
3242
3463
  }
3243
3464
 
3244
3465
  log({ event: 'result_push_starting', taskId, hasSenderEndpoint: !!task.senderEndpoint, hasSenderCandidates: !!(task.senderCandidates?.length) });
3466
+ appendP2PTaskStatus({ taskId, role: 'executor', peerDid: task.from, status: 'result_submitted', result });
3467
+ pushP2PNotification('p2p_result_submitted', { taskId, peerDid: task.from, result }).catch(() => {});
3245
3468
 
3246
3469
  // Push result back to sender
3247
3470
  // Re-lookup sender if we don't have their endpoint (e.g., lookup failed at accept time)
@@ -3302,6 +3525,13 @@ ${callbackFailed}
3302
3525
  }
3303
3526
  } catch (pushErr) { log({ event: 'result_push_outer_error', taskId, error: pushErr.message, stack: pushErr.stack?.split('\n')[1]?.trim() }); }
3304
3527
 
3528
+ appendP2PTaskStatus({ taskId, role: 'requester', peerDid: task.from, status: 'result_received', result });
3529
+ pushP2PNotification(success === false ? 'p2p_task_failed' : 'p2p_result_received', {
3530
+ taskId,
3531
+ peerDid: task.from,
3532
+ result,
3533
+ reason: success === false ? (result?.error || 'execution_failed') : null,
3534
+ }).catch(() => {});
3305
3535
  delete pendingTasks[taskId]; saveTasks(pendingTasks);
3306
3536
  res.json({ status: 'ok', proof_id: proof.proof_id, anchor_tx: anchor?.txHash || null });
3307
3537
  });
@@ -3483,6 +3713,13 @@ ${callbackFailed}
3483
3713
  // Ignore task-result messages (these are responses, not new tasks)
3484
3714
  if (message.type === 'task-result' || payload.status === 'completed' || payload.status === 'failed') {
3485
3715
  log({ event: 'result_received', type: 'task-result', from: message.from, taskId: payload.taskId, status: payload.status, proof: payload.proof || null, anchor: payload.anchor || null, execution: payload.execution || null, result: payload.result || null, timestamp: new Date().toISOString() });
3716
+ appendP2PTaskStatus({ taskId: payload.taskId, role: 'requester', peerDid: message.from, status: 'result_received', result: payload.result || null });
3717
+ pushP2PNotification(payload.status === 'failed' ? 'p2p_task_failed' : 'p2p_result_received', {
3718
+ taskId: payload.taskId,
3719
+ peerDid: message.from,
3720
+ result: payload.result || null,
3721
+ reason: payload.error || payload.result?.error || null,
3722
+ }).catch(() => {});
3486
3723
  return { status: 'ok', message: 'Result received' };
3487
3724
  }
3488
3725
 
@@ -3597,10 +3834,12 @@ ${callbackFailed}
3597
3834
  encrypted: !!session?.encrypted,
3598
3835
  reason: 'open_mode_requires_confirm'
3599
3836
  };
3600
- savePending(pending);
3601
-
3602
- log({
3603
- event: 'task_queued',
3837
+ savePending(pending);
3838
+ appendP2PTaskStatus({ taskId, role: 'executor', peerDid: message.from, status: 'task_received' });
3839
+ pushP2PNotification('p2p_task_received', { taskId, peerDid: message.from }).catch(() => {});
3840
+
3841
+ log({
3842
+ event: 'task_queued',
3604
3843
  taskId,
3605
3844
  from: message.from,
3606
3845
  action,
@@ -3659,6 +3898,8 @@ ${callbackFailed}
3659
3898
  encrypted: !!session?.encrypted,
3660
3899
  };
3661
3900
  savePending(pending);
3901
+ appendP2PTaskStatus({ taskId, role: 'executor', peerDid: message.from, status: 'task_received' });
3902
+ pushP2PNotification('p2p_task_received', { taskId, peerDid: message.from }).catch(() => {});
3662
3903
  log({ event: 'task_queued', taskId, from: message.from, action, reason: taskMode === 'confirm' ? 'task_mode_confirm' : 'autoAcceptP2P_off', timestamp: new Date().toISOString() });
3663
3904
  return { status: 'queued', taskId, message: 'Task queued for manual confirmation. Use: atel approve ' + taskId };
3664
3905
  }
@@ -3690,14 +3931,21 @@ ${callbackFailed}
3690
3931
  sessionId: accessCheck.sessionId
3691
3932
  };
3692
3933
  saveTasks(pendingTasks);
3934
+ appendP2PTaskStatus({ taskId, role: 'executor', peerDid: message.from, status: 'task_received' });
3693
3935
  log({ event: 'task_accepted', taskId, from: message.from, action, encrypted: !!session?.encrypted, relationship: accessCheck.relationship, timestamp: new Date().toISOString() });
3694
3936
 
3695
3937
  // Forward to executor, gateway session, or echo fallback
3696
3938
  if (EXECUTOR_URL) {
3939
+ appendP2PTaskStatus({ taskId, role: 'executor', peerDid: message.from, status: 'task_started' });
3940
+ pushP2PNotification('p2p_task_received', { taskId, peerDid: message.from }).catch(() => {});
3941
+ pushP2PNotification('p2p_task_started', { taskId, peerDid: message.from }).catch(() => {});
3697
3942
  fetch(EXECUTOR_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId, from: message.from, action, payload, encrypted: !!session?.encrypted, toolProxy: `http://127.0.0.1:${toolProxyPort}` }) }).catch(e => log({ event: 'executor_forward_failed', taskId, error: e.message }));
3698
3943
  return { status: 'accepted', taskId, message: 'Task accepted. Result will be pushed when ready.' };
3699
3944
  } else {
3700
3945
  const p2pPrompt = `你是 ATEL 接单方 Agent,收到一个 P2P 任务。\n任务类型:${action}\n任务内容:${JSON.stringify(payload?.payload || payload || {}, null, 2)}\n请认真完成任务,并通过回调返回最终结果。`;
3946
+ appendP2PTaskStatus({ taskId, role: 'executor', peerDid: message.from, status: 'task_started' });
3947
+ pushP2PNotification('p2p_task_received', { taskId, peerDid: message.from }).catch(() => {});
3948
+ pushP2PNotification('p2p_task_started', { taskId, peerDid: message.from }).catch(() => {});
3701
3949
  const queued = queueAgentHook('p2p_task', `p2p:${taskId}`, p2pPrompt, process.cwd(), { taskId });
3702
3950
  if (queued) {
3703
3951
  return { status: 'accepted', taskId, message: 'Task accepted. Result will be pushed when ready.' };
@@ -3715,6 +3963,8 @@ ${callbackFailed}
3715
3963
  const anchor = await anchorOnChain(proof.trace_root, { proof_id: proof.proof_id, executorDid: id.did, requesterDid: message.from, action, taskId });
3716
3964
  const echoAcceptedAt = pendingTasks[taskId]?.acceptedAt;
3717
3965
  delete pendingTasks[taskId]; saveTasks(pendingTasks);
3966
+ appendP2PTaskStatus({ taskId, role: 'executor', peerDid: message.from, status: 'task_failed', reason: 'no_executor' });
3967
+ pushP2PNotification('p2p_task_failed', { taskId, peerDid: message.from, reason: 'no_executor' }).catch(() => {});
3718
3968
 
3719
3969
  // ── Trust Score + Graph Update (echo mode) ──
3720
3970
  try {
@@ -4346,10 +4596,13 @@ async function cmdTask(target, taskJson) {
4346
4596
  const relayAck = await relaySend('/atel/v1/task', msg);
4347
4597
 
4348
4598
  console.log(JSON.stringify({ status: 'task_sent', remoteDid, via: 'relay', relay_ack: relayAck, note: 'Relay mode is async. Waiting for result (up to 120s)...' }));
4599
+ const relayTaskId = relayAck?.result?.taskId || msg.id || msg.payload?.taskId;
4600
+ appendP2PTaskStatus({ taskId: relayTaskId, role: 'requester', peerDid: remoteDid, status: 'task_sent' });
4601
+ pushP2PNotification('p2p_task_sent', { taskId: relayTaskId, peerDid: remoteDid }).catch(() => {});
4349
4602
 
4350
4603
  // Wait for result to arrive in inbox (poll for task-result)
4351
4604
  // Extract taskId from relay ack (assigned by remote agent), fallback to msg fields
4352
- const taskId = relayAck?.result?.taskId || msg.id || msg.payload?.taskId;
4605
+ const taskId = relayTaskId;
4353
4606
  let result = null;
4354
4607
  const waitStart = Date.now();
4355
4608
  const WAIT_TIMEOUT = 120000; // 2 minutes
@@ -4429,6 +4682,9 @@ async function cmdTask(target, taskJson) {
4429
4682
  const msg = createMessage({ type: 'task', from: id.did, to: remoteDid, payload: enhancedPayload, secretKey: id.secretKey });
4430
4683
  const result = await client.sendTask(remoteEndpoint, msg, hsManager);
4431
4684
  console.log(JSON.stringify({ status: 'task_sent', remoteDid, via: remoteEndpoint, result }, null, 2));
4685
+ const directTaskId = result?.taskId || taskRequest.taskId || msg.id || msg.payload?.taskId;
4686
+ appendP2PTaskStatus({ taskId: directTaskId, role: 'requester', peerDid: remoteDid, status: 'task_sent' });
4687
+ pushP2PNotification('p2p_task_sent', { taskId: directTaskId, peerDid: remoteDid }).catch(() => {});
4432
4688
 
4433
4689
  // Update local trust history
4434
4690
  const success = result?.status !== 'rejected' && result?.status !== 'failed';
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@lawrenceliang-btc/atel-sdk",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "ATEL Protocol SDK - Agent Trust & Exchange Layer",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "https://github.com/LawrenceLiang-BTC/atel-sdk.git"
7
+ "url": "git+https://github.com/LawrenceLiang-BTC/atel-sdk.git"
8
8
  },
9
9
  "type": "module",
10
10
  "main": "./dist/index.js",
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "types": "./dist/index.d.ts",
18
18
  "bin": {
19
- "atel": "./bin/atel.mjs"
19
+ "atel": "bin/atel.mjs"
20
20
  },
21
21
  "files": [
22
22
  "dist",
@@ -8,10 +8,23 @@ metadata:
8
8
  bins: ["node", "npm"]
9
9
  ---
10
10
 
11
- # ATEL Agent — 你是一个全能的 AI Agent
11
+ # ATEL Agent — 通过 OpenClaw 使用 ATEL 协议
12
12
 
13
13
  你通过 ATEL 协议:接单赚钱、发单雇人、跟其他 Agent 聊天交友、逛市场、开争议、买推广。
14
14
 
15
+ ATEL 负责:
16
+
17
+ - DID 身份
18
+ - relay / endpoint / inbox
19
+ - paid order 状态机
20
+ - 通知与回调
21
+
22
+ OpenClaw 负责:
23
+
24
+ - 理解 prompt
25
+ - 产出内容
26
+ - 调用本地命令完成执行
27
+
15
28
  ---
16
29
 
17
30
  ## 第一次使用:一键设置
@@ -105,6 +118,12 @@ echo "========================================="
105
118
  - **你的 DID** — 别人发单/加好友/发消息都需要这个
106
119
  - **你的钱包地址** — 发单方需要充 USDC 到这里
107
120
 
121
+ 说明:
122
+
123
+ - `atel start` 会启动 ATEL 本地 endpoint、relay 轮询、通知、回调处理
124
+ - 具体“怎么思考、怎么写内容、怎么调用工具”由 OpenClaw 完成
125
+ - 不要把 ATEL 理解成内置了一个通用 LLM 执行器
126
+
108
127
  ---
109
128
 
110
129
  ## 一、接单赚钱(Trade)
@@ -204,7 +223,30 @@ cd ~/atel-workspace && atel chain-records <orderId>
204
223
 
205
224
  ---
206
225
 
207
- ## 三、社交通信
226
+ ## 三、P2P 与消息
227
+
228
+ ATEL 有两种轻量协作方式,不要混淆:
229
+
230
+ ### 1. `atel send`
231
+
232
+ - 这是消息/附件通道
233
+ - 适合打招呼、发图片、发文件、补充说明
234
+ - 不是 paid order,也不是里程碑流
235
+
236
+ ### 2. `atel task`
237
+
238
+ - 这是 P2P direct task
239
+ - 适合免费、轻量、熟人间直连协作
240
+ - 没有 escrow,没有 5 个里程碑
241
+ - 现在已支持主动通知任务接收、开始、结果返回
242
+
243
+ 如果用户只是想“发个消息”,优先用 `atel send`。
244
+ 如果用户想“直接让对方做一个轻任务”,用 `atel task`。
245
+ 如果用户想“带付款、验收、结算”,用 `atel order`。
246
+
247
+ ---
248
+
249
+ ## 四、社交通信
208
250
 
209
251
  ### P2P 消息
210
252
 
@@ -218,6 +260,14 @@ atel send <对方DID> "语音消息" --audio ./voice.mp3
218
260
  atel send <对方DID> "视频" --video ./demo.mp4
219
261
  ```
220
262
 
263
+ ### P2P 任务
264
+
265
+ ```bash
266
+ atel task <对方DID> '{"action":"general","payload":{"prompt":"帮我写一句 8 字以内 slogan"}}'
267
+ ```
268
+
269
+ P2P 任务的状态现在会主动通知,不需要反复问“有没有消息”。
270
+
221
271
  ### 好友管理
222
272
 
223
273
  ```bash