@lawrenceliang-btc/atel-sdk 1.1.8 → 1.1.10

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
@@ -21,6 +21,10 @@ ATEL provides the cryptographic primitives and protocol building blocks that ena
21
21
  - ATEL handles DID identity, relay, inbox, callback, notification, and paid order state
22
22
  - OpenClaw or your own runtime handles reasoning and tool use
23
23
  - Cross-platform CLI (Linux/macOS/Windows)
24
+ - Paid Platform orders currently support two settlement chains:
25
+ - `Base`
26
+ - `BSC`
27
+ - For paid orders, the chain truth source is always `order.chain`
24
28
 
25
29
  ### P2P Friend System
26
30
  - Relationship-based access control (friends-only mode)
@@ -59,6 +63,14 @@ atel register "My Agent" "assistant,research"
59
63
  atel start 3100
60
64
  ```
61
65
 
66
+ If you want to support paid Platform orders on EVM chains, configure at least one paid-order chain key before or after registering:
67
+
68
+ ```bash
69
+ export ATEL_BASE_PRIVATE_KEY=...
70
+ # or
71
+ export ATEL_BSC_PRIVATE_KEY=...
72
+ ```
73
+
62
74
  ### Recommended Runtime
63
75
 
64
76
  ATEL is not a built-in general-purpose LLM executor. The recommended setup is:
@@ -76,6 +88,8 @@ atel start 3100
76
88
 
77
89
  For custom runtimes, point `ATEL_EXECUTOR_URL` at your own service.
78
90
 
91
+ For paid orders, do not hardcode Base as the only chain. Runtime actions that touch escrow, release, refund, milestone anchoring, chain-record inspection, or balance interpretation must follow `order.chain`.
92
+
79
93
  ## Architecture
80
94
 
81
95
  ATEL is organized into protocol and runtime layers:
@@ -170,6 +184,17 @@ atel milestone-submit <orderId> <index> --result # Submit milestone result
170
184
  atel milestone-verify <orderId> <index> --pass # Verify submitted milestone
171
185
  ```
172
186
 
187
+ Notes:
188
+
189
+ - Paid Platform orders are currently supported on `Base` and `BSC`
190
+ - Before acting on a paid order, inspect `atel order-info <orderId>` or `atel milestone-status <orderId>`
191
+ - Treat `order.chain` as the only source of truth for:
192
+ - smart wallet
193
+ - escrow
194
+ - release / refund
195
+ - chain-records
196
+ - chain-side balance interpretation
197
+
173
198
  ## API Examples
174
199
 
175
200
  ### Identity & Signing
package/bin/atel.mjs CHANGED
@@ -52,7 +52,7 @@
52
52
  */
53
53
 
54
54
  import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
55
- import { resolve, join } from 'node:path';
55
+ import { resolve, join, dirname } from 'node:path';
56
56
  import crypto from 'node:crypto';
57
57
  import {
58
58
  AgentIdentity, AgentEndpoint, AgentClient, HandshakeManager,
@@ -91,6 +91,7 @@ const NOTIFY_TARGETS_FILE = resolve(ATEL_DIR, 'notify-targets.json');
91
91
  const TRADE_TRACK_FILE = resolve(ATEL_DIR, 'tracked-orders.json');
92
92
  const P2P_STATUS_FILE = resolve(ATEL_DIR, 'p2p-task-status.jsonl');
93
93
  const PENDING_AGENT_CALLBACKS_FILE = resolve(ATEL_DIR, 'pending-agent-callbacks.json');
94
+ const ORDER_WORK_DIR = resolve(ATEL_DIR, 'order-workspaces');
94
95
  const KEYS_DIR = resolve(ATEL_DIR, 'keys');
95
96
  const ANCHOR_FILE = resolve(KEYS_DIR, 'anchor.json');
96
97
 
@@ -100,6 +101,111 @@ const DEFAULT_POLICY = { rateLimit: 60, maxPayloadBytes: 1048576, maxConcurrent:
100
101
 
101
102
  function ensureDir() { if (!existsSync(ATEL_DIR)) mkdirSync(ATEL_DIR, { recursive: true }); }
102
103
 
104
+ function ensureOrderWorkspace(orderId, context = {}) {
105
+ ensureDir();
106
+ const safeOrderId = String(orderId || 'unknown').replace(/[^a-zA-Z0-9._-]/g, '_');
107
+ const dir = resolve(ORDER_WORK_DIR, safeOrderId);
108
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
109
+ const contextFile = join(dir, 'ORDER_CONTEXT.md');
110
+ const lines = [
111
+ `# ATEL Order Context`,
112
+ ``,
113
+ `Order ID: ${orderId || ''}`,
114
+ `Chain: ${context.chain || ''}`,
115
+ `Role: ${context.role || ''}`,
116
+ `Status: ${context.status || ''}`,
117
+ `Phase: ${context.phase || ''}`,
118
+ `Current Milestone: ${context.currentMilestone ?? ''}`,
119
+ `Milestone Title: ${context.milestoneTitle || ''}`,
120
+ ``,
121
+ `## Order Description`,
122
+ context.orderDescription || '',
123
+ ``,
124
+ `## Milestone Objective`,
125
+ context.milestoneObjective || '',
126
+ ``,
127
+ `## Submission Content`,
128
+ context.resultSummary || '',
129
+ ``,
130
+ `## Previous Approved Outputs`,
131
+ context.previousApprovedOutputs || '',
132
+ ``,
133
+ `## Hard Rules`,
134
+ `- Only work from the order description and milestone objective in this file.`,
135
+ `- Do not inspect unrelated local projects or repository content unless the order explicitly asks for repo analysis.`,
136
+ `- Do not infer a different project from stray files in the machine workspace.`,
137
+ `- Return only content that directly satisfies this order.`,
138
+ ``,
139
+ ];
140
+ writeFileSync(contextFile, lines.join('\n'));
141
+ return { dir, contextFile };
142
+ }
143
+
144
+ function getOrderWorkspace(orderId, context = {}) {
145
+ if (!orderId) return { dir: process.cwd(), contextFile: '' };
146
+ return ensureOrderWorkspace(orderId, context);
147
+ }
148
+
149
+ function getAtelWorkspaceRoot() {
150
+ return dirname(ATEL_DIR);
151
+ }
152
+
153
+ function shouldAllowRepoAccess(context = {}) {
154
+ const description = String(context?.orderDescription || '').toLowerCase();
155
+ const objective = String(context?.milestoneObjective || '').toLowerCase();
156
+ const resultSummary = String(context?.resultSummary || '').toLowerCase();
157
+ const previousApprovedOutputs = String(context?.previousApprovedOutputs || '').toLowerCase();
158
+ const combined = `${description}\n${objective}\n${resultSummary}\n${previousApprovedOutputs}`;
159
+ return /(repo|repository|codebase|仓库|代码库|项目代码|source code|read files|analyze code|修改代码|修复代码|实现功能)/i.test(combined);
160
+ }
161
+
162
+ function summarizeApprovedMilestones(milestones = [], beforeIndex = Number.MAX_SAFE_INTEGER) {
163
+ return (Array.isArray(milestones) ? milestones : [])
164
+ .filter((m) => m && m.status === 'verified' && Number.isFinite(m.index) && m.index < beforeIndex)
165
+ .sort((a, b) => a.index - b.index)
166
+ .map((m) => `M${m.index}: ${m.title || ''}\nResult: ${m.resultSummary || ''}`.trim())
167
+ .join('\n\n');
168
+ }
169
+
170
+ function sanitizeAgentPrompt(promptText, meta = {}) {
171
+ const raw = typeof promptText === 'string' ? promptText : '';
172
+ const trimmed = raw.trim();
173
+ if (!trimmed) {
174
+ log({ event: 'agent_prompt_skip_empty', eventType: meta.eventType || 'unknown', dedupeKey: meta.dedupeKey || '' });
175
+ return '';
176
+ }
177
+
178
+ // Keep a large but bounded margin below upstream model limits. We only need
179
+ // concise task prompts here; oversized prompts add noise and can trigger
180
+ // upstream length validation errors.
181
+ const maxChars = 16000;
182
+ if (trimmed.length <= maxChars) return trimmed;
183
+
184
+ const truncated = `${trimmed.slice(0, maxChars)}\n\n[Prompt truncated by ATEL SDK to stay within model input limits.]`;
185
+ log({
186
+ event: 'agent_prompt_truncated',
187
+ eventType: meta.eventType || 'unknown',
188
+ dedupeKey: meta.dedupeKey || '',
189
+ originalChars: trimmed.length,
190
+ finalChars: truncated.length,
191
+ });
192
+ return truncated;
193
+ }
194
+
195
+ function isKnownUpstreamModelInputError(text) {
196
+ const value = String(text || '');
197
+ return value.includes('InternalError.Algo.InvalidParameter')
198
+ || value.includes('Range of input length should be [1, 258048]');
199
+ }
200
+
201
+ function summarizeAgentOutput(text, maxChars = 300) {
202
+ const raw = String(text || '');
203
+ const trimmed = raw.trim();
204
+ if (!trimmed) return '';
205
+ if (isKnownUpstreamModelInputError(trimmed)) return '[suppressed upstream model input-length error]';
206
+ return trimmed.substring(0, maxChars);
207
+ }
208
+
103
209
  // ═══════════════════════════════════════════════════════════════════
104
210
  // Notification Target System — auto-discover gateway, manage targets
105
211
  // ═══════════════════════════════════════════════════════════════════
@@ -348,6 +454,18 @@ async function executeRecommendedActionDirect(eventType, action, cwd, dedupeKey)
348
454
  if (command.length === 0) {
349
455
  return { ok: false, skipped: true, reason: 'empty_command' };
350
456
  }
457
+ const actionKey = JSON.stringify(command);
458
+ if (globalThis.__atelActiveDirectActionKeys?.has(actionKey)) {
459
+ log({
460
+ event: 'recommended_action_direct_skip',
461
+ eventType,
462
+ dedupeKey,
463
+ action: action.action,
464
+ command,
465
+ reason: 'inflight_duplicate',
466
+ });
467
+ return { ok: true, skipped: true, reason: 'inflight_duplicate' };
468
+ }
351
469
 
352
470
  // Idempotency guard: short-circuit duplicate milestone plan/submit/verify actions
353
471
  // if the order or milestone has already advanced past the required state.
@@ -384,8 +502,10 @@ async function executeRecommendedActionDirect(eventType, action, cwd, dedupeKey)
384
502
  }
385
503
  const milestone = needsMilestoneIndex && Array.isArray(state?.milestones) ? state.milestones.find((m) => m.index === index) : null;
386
504
  if (needsMilestoneIndex && milestone) {
387
- const expectedStatus = command[1] === 'milestone-verify' ? 'submitted' : 'pending';
388
- if (milestone.status !== expectedStatus) {
505
+ const expectedStatuses = command[1] === 'milestone-verify'
506
+ ? ['submitted']
507
+ : (eventType === 'milestone_rejected' ? ['pending', 'rejected'] : ['pending']);
508
+ if (!expectedStatuses.includes(milestone.status)) {
389
509
  log({
390
510
  event: 'recommended_action_direct_skip',
391
511
  eventType,
@@ -414,12 +534,68 @@ async function executeRecommendedActionDirect(eventType, action, cwd, dedupeKey)
414
534
  const { execFile } = await import('child_process');
415
535
  const childCmd = command[0] === 'atel' ? process.execPath : command[0];
416
536
  const childArgs = command[0] === 'atel' ? [process.argv[1], ...command.slice(1)] : command.slice(1);
537
+ const childCwd = command[0] === 'atel' ? getAtelWorkspaceRoot() : cwd;
417
538
 
418
539
  log({ event: 'recommended_action_direct_trigger', eventType, dedupeKey, action: action.action, command });
540
+ globalThis.__atelActiveDirectActionKeys ??= new Set();
541
+ globalThis.__atelActiveDirectActionKeys.add(actionKey);
419
542
 
420
543
  return await new Promise((resolve) => {
421
- execFile(childCmd, childArgs, { timeout: 180000, cwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
544
+ execFile(childCmd, childArgs, { timeout: 180000, cwd: childCwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
545
+ globalThis.__atelActiveDirectActionKeys?.delete(actionKey);
422
546
  if (err) {
547
+ const combinedErrorText = String(stderr || err.message || '');
548
+ if (
549
+ command[0] === 'atel' &&
550
+ command[1] === 'milestone-feedback' &&
551
+ command.includes('--approve') &&
552
+ combinedErrorText.includes('order not in milestone_review status')
553
+ ) {
554
+ log({
555
+ event: 'recommended_action_direct_skip',
556
+ eventType,
557
+ dedupeKey,
558
+ action: action.action,
559
+ command,
560
+ reason: 'order_status_executing',
561
+ });
562
+ resolve({ ok: true, skipped: true, reason: 'order_status_executing' });
563
+ return;
564
+ }
565
+ if (
566
+ command[0] === 'atel' &&
567
+ command[1] === 'milestone-submit' &&
568
+ combinedErrorText.includes('milestone cannot be submitted in status: submitted')
569
+ ) {
570
+ log({
571
+ event: 'recommended_action_direct_skip',
572
+ eventType,
573
+ dedupeKey,
574
+ action: action.action,
575
+ command,
576
+ reason: 'milestone_status_submitted',
577
+ });
578
+ resolve({ ok: true, skipped: true, reason: 'milestone_status_submitted' });
579
+ return;
580
+ }
581
+ if (
582
+ command[0] === 'atel' &&
583
+ command[1] === 'milestone-verify' &&
584
+ (combinedErrorText.includes('milestone cannot be verified in status: verified') ||
585
+ combinedErrorText.includes('milestone not in submitted status') ||
586
+ combinedErrorText.includes('milestone cannot be verified in status: settled'))
587
+ ) {
588
+ log({
589
+ event: 'recommended_action_direct_skip',
590
+ eventType,
591
+ dedupeKey,
592
+ action: action.action,
593
+ command,
594
+ reason: 'milestone_already_processed',
595
+ });
596
+ resolve({ ok: true, skipped: true, reason: 'milestone_already_processed' });
597
+ return;
598
+ }
423
599
  log({
424
600
  event: 'recommended_action_direct_error',
425
601
  eventType,
@@ -439,7 +615,7 @@ async function executeRecommendedActionDirect(eventType, action, cwd, dedupeKey)
439
615
  dedupeKey,
440
616
  action: action.action,
441
617
  command,
442
- stdout: (stdout || '').substring(0, 400),
618
+ stdout: summarizeAgentOutput(stdout, 400),
443
619
  });
444
620
  resolve({ ok: true, stdout });
445
621
  });
@@ -2574,7 +2750,9 @@ async function cmdStart(port) {
2574
2750
  return;
2575
2751
  }
2576
2752
  }
2577
- res.status(404).json({ error: 'Unknown dedupeKey' });
2753
+ // Treat late/duplicate/expired callbacks as idempotent skips rather than hard errors.
2754
+ // The callback source has already completed, timed out, or been recovered elsewhere.
2755
+ res.json({ status: 'ok', skipped: true, reason: 'unknown_or_expired_dedupeKey' });
2578
2756
  return;
2579
2757
  }
2580
2758
  log({
@@ -2588,6 +2766,67 @@ async function cmdStart(port) {
2588
2766
  });
2589
2767
 
2590
2768
  if (body.status === 'failed') {
2769
+ // Some subagents pessimistically send `failed` after already producing a usable
2770
+ // summary/result because they observed a callback transport error on their side.
2771
+ // If the payload is still actionable, recover it here instead of dropping the flow.
2772
+ const failedAction = buildAgentCallbackAction(pending.eventType, pending.payload || {}, body);
2773
+ if (failedAction.ok && !failedAction.skipped) {
2774
+ log({
2775
+ event: 'agent_callback_failed_recovered',
2776
+ eventType: pending.eventType,
2777
+ dedupeKey,
2778
+ childSessionKey: pending.childSessionKey,
2779
+ summary: body.summary,
2780
+ error: body.error,
2781
+ });
2782
+
2783
+ if (failedAction.action?.type === 'local_result') {
2784
+ try {
2785
+ const localResp = await fetch(`http://127.0.0.1:${p}/atel/v1/result`, {
2786
+ method: 'POST',
2787
+ headers: { 'Content-Type': 'application/json' },
2788
+ body: JSON.stringify({
2789
+ taskId: failedAction.action.taskId,
2790
+ result: failedAction.action.result,
2791
+ success: true,
2792
+ }),
2793
+ signal: AbortSignal.timeout(15000),
2794
+ });
2795
+ const localBody = await localResp.json().catch(() => ({}));
2796
+ pendingAgentCallbacks.delete(dedupeKey);
2797
+ if (localResp.ok) {
2798
+ markPersistedPendingAgentCallbackCompleted(dedupeKey, { eventType: pending.eventType, payload: pending.payload, cwd: pending.cwd, source: dedupeKey?.startsWith('reconcile:') ? 'reconcile' : 'main', action: failedAction.action, localBody, recoveredFromFailed: true });
2799
+ pending.resolve({ ok: true, recovered: true, body, action: failedAction.action, localBody });
2800
+ res.json({ status: 'ok', recovered: true });
2801
+ return;
2802
+ }
2803
+ clearPersistedPendingAgentCallback(dedupeKey);
2804
+ pending.resolve({ ok: false, body, action: failedAction.action, localBody, error: localBody.error || 'local_result_callback_failed' });
2805
+ res.status(500).json({ error: localBody.error || 'local_result_callback_failed' });
2806
+ return;
2807
+ } catch (e) {
2808
+ pendingAgentCallbacks.delete(dedupeKey);
2809
+ clearPersistedPendingAgentCallback(dedupeKey);
2810
+ pending.resolve({ ok: false, body, action: failedAction.action, error: e.message });
2811
+ res.status(500).json({ error: e.message || 'local_result_callback_failed' });
2812
+ return;
2813
+ }
2814
+ }
2815
+
2816
+ const execResult = await executeRecommendedActionDirect(pending.eventType, failedAction.action, pending.cwd || process.cwd(), dedupeKey);
2817
+ pendingAgentCallbacks.delete(dedupeKey);
2818
+ if (execResult.ok) {
2819
+ markPersistedPendingAgentCallbackCompleted(dedupeKey, { eventType: pending.eventType, payload: pending.payload, cwd: pending.cwd, source: dedupeKey?.startsWith('reconcile:') ? 'reconcile' : 'main', action: failedAction.action, recoveredFromFailed: true });
2820
+ pending.resolve({ ok: true, recovered: true, body, action: failedAction.action, execResult });
2821
+ res.json({ status: 'ok', recovered: true });
2822
+ return;
2823
+ }
2824
+ clearPersistedPendingAgentCallback(dedupeKey);
2825
+ pending.resolve({ ok: false, body, action: failedAction.action, execResult, error: execResult.error || 'callback_action_failed' });
2826
+ res.status(500).json({ error: execResult.error || 'callback_action_failed' });
2827
+ return;
2828
+ }
2829
+
2591
2830
  pendingAgentCallbacks.delete(dedupeKey);
2592
2831
  clearPersistedPendingAgentCallback(dedupeKey);
2593
2832
  pending.resolve({ ok: false, body, error: body.error || 'agent_reported_failed' });
@@ -2661,7 +2900,7 @@ async function cmdStart(port) {
2661
2900
  res.json({ status: 'ok' });
2662
2901
  });
2663
2902
 
2664
- function buildGatewayCallbackPrompt(eventType, promptText, callbackUrl, dedupeKey, cwd) {
2903
+ function buildGatewayCallbackPrompt(eventType, promptText, callbackUrl, dedupeKey, cwd, payload = {}) {
2665
2904
  const callbackExamples = {
2666
2905
  milestone_submitted: [
2667
2906
  '通过时执行:',
@@ -2701,18 +2940,30 @@ async function cmdStart(port) {
2701
2940
  ].join('\n');
2702
2941
 
2703
2942
  const callbackDone = eventType === 'milestone_submitted' ? callbackExamples.milestone_submitted : callbackExamples.default;
2943
+ const contextFile = join(cwd, 'ORDER_CONTEXT.md');
2944
+ const allowRepoAccess = shouldAllowRepoAccess(payload);
2945
+ const fileAccessRule = allowRepoAccess
2946
+ ? `3. 仅允许使用目录 ${cwd} 下与当前订单直接相关的内容;如果订单明确要求读仓库,也只能读取该订单工作区中明确提供的路径。`
2947
+ : `3. 本单禁止读取任何本地文件、共享草稿、仓库或其他项目。不要使用文件搜索、目录浏览、读文件等方式扩展上下文;只允许依据本条消息中的订单描述、里程碑目标、提交内容来工作。`;
2948
+ const contextRule = allowRepoAccess
2949
+ ? `4. 优先读取 ${contextFile},严格以其中的订单描述、里程碑目标、提交内容为准。`
2950
+ : `4. 不要扫描本机其他目录,不要读取 /root/atel-workspace 下的共享文件,不要根据历史项目或 stray files 推断任务。`;
2951
+ const repoRule = allowRepoAccess
2952
+ ? `5. 只有当订单明确要求分析仓库/代码时,才允许读取订单工作区里显式提供的代码路径;禁止顺带读取其他目录。`
2953
+ : `5. 本单不是 repo/code 任务。禁止把任务扩展成代码分析、工程改造或共享草稿筛选。`;
2704
2954
  if (eventType === 'p2p_task') {
2705
2955
  return `${promptText}
2706
2956
 
2707
2957
  重要要求:
2708
2958
  1. 这是一个 P2P 任务。不要调用 atel result;本地 SDK 会在你回调后自动提交结果。
2709
2959
  2. 你的任务是认真完成 AI 工作,并把最终结论通过回调发回本地 SDK。
2710
- 3. 你可以在目录 ${cwd} 下阅读文件、运行分析命令、整理内容,但不要自己推进 ATEL 状态机。
2711
- 4. 完成后,必须立刻执行下面这个成功回调命令模板,并把其中内容替换成你的真实结果:
2960
+ ${fileAccessRule}
2961
+ ${contextRule}
2962
+ 5. 完成后,必须立刻执行下面这个成功回调命令模板,并把其中内容替换成你的真实结果:
2712
2963
 
2713
2964
  ${callbackDone}
2714
2965
 
2715
- 5. 如果重试后仍然失败,也必须执行下面这个失败回调命令:
2966
+ 6. 如果重试后仍然失败,也必须执行下面这个失败回调命令:
2716
2967
 
2717
2968
  ${callbackFailed}
2718
2969
  `;
@@ -2723,17 +2974,191 @@ ${callbackFailed}
2723
2974
  重要要求:
2724
2975
  1. 不要执行 atel milestone-submit / milestone-verify / milestone-feedback 命令;这些命令会由本地 SDK 在你回调后代为执行。
2725
2976
  2. 你的任务是认真完成 AI 工作,并把最终结论通过回调发回本地 SDK。
2726
- 3. 你可以在目录 ${cwd} 下阅读文件、运行分析命令、整理内容,但最终不要自己推进 ATEL 状态机。
2727
- 4. 完成后,必须立刻执行下面这个成功回调命令模板,并把其中内容替换成你的真实结果:
2977
+ ${fileAccessRule}
2978
+ ${contextRule}
2979
+ ${repoRule}
2980
+ 6. 完成后,必须立刻执行下面这个成功回调命令模板,并把其中内容替换成你的真实结果:
2728
2981
 
2729
2982
  ${callbackDone}
2730
2983
 
2731
- 5. 如果重试后仍然失败,也必须执行下面这个失败回调命令:
2984
+ 7. 如果重试后仍然失败,也必须执行下面这个失败回调命令:
2732
2985
 
2733
2986
  ${callbackFailed}
2734
2987
  `;
2735
2988
  }
2736
2989
 
2990
+ function buildLocalAgentPrompt(eventType, promptText) {
2991
+ if (eventType === 'milestone_submitted') {
2992
+ return `${promptText}
2993
+
2994
+ 重要:你不是在和用户聊天。
2995
+ 不要输出 markdown、代码块、解释、分析过程。
2996
+ 你必须只输出一行 JSON。
2997
+
2998
+ 通过时:
2999
+ {"decision":"pass","summary":"简短通过原因"}
3000
+
3001
+ 拒绝时:
3002
+ {"decision":"reject","reason":"具体拒绝原因","summary":"简短审核结论"}`;
3003
+ }
3004
+
3005
+ if (['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected'].includes(eventType)) {
3006
+ return `${promptText}
3007
+
3008
+ 重要:你不是在和用户聊天。
3009
+ 不要输出 markdown、标题、项目符号、解释、分析过程。
3010
+ 你必须只输出一行 JSON。
3011
+
3012
+ 格式:
3013
+ {"result":"当前里程碑的真实交付内容"}`;
3014
+ }
3015
+
3016
+ return promptText;
3017
+ }
3018
+
3019
+ function normalizeLocalAgentStdout(stdout) {
3020
+ const text = String(stdout || '').trim();
3021
+ if (!text) return '';
3022
+ try {
3023
+ const parsed = JSON.parse(text);
3024
+ if (Array.isArray(parsed?.payloads)) {
3025
+ for (const item of parsed.payloads) {
3026
+ const candidate = String(item?.text || '').trim();
3027
+ if (candidate) return candidate;
3028
+ }
3029
+ }
3030
+ if (typeof parsed?.text === 'string' && parsed.text.trim()) return parsed.text.trim();
3031
+ if (typeof parsed?.result === 'string' && parsed.result.trim()) return JSON.stringify({ result: parsed.result.trim() });
3032
+ if (typeof parsed?.decision === 'string') return JSON.stringify(parsed);
3033
+ } catch {}
3034
+ const fencedJson = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
3035
+ if (fencedJson?.[1]) return fencedJson[1].trim();
3036
+ const jsonObjects = text.match(/\{[\s\S]*\}/g);
3037
+ if (jsonObjects?.length) {
3038
+ for (let i = jsonObjects.length - 1; i >= 0; i -= 1) {
3039
+ const candidate = jsonObjects[i].trim();
3040
+ try {
3041
+ const parsed = JSON.parse(candidate);
3042
+ if (Array.isArray(parsed?.payloads)) {
3043
+ for (const item of parsed.payloads) {
3044
+ const nested = String(item?.text || '').trim();
3045
+ if (nested) return nested;
3046
+ }
3047
+ }
3048
+ if (typeof parsed?.text === 'string' && parsed.text.trim()) return parsed.text.trim();
3049
+ if (typeof parsed?.result === 'string' && parsed.result.trim()) return JSON.stringify({ result: parsed.result.trim() });
3050
+ if (typeof parsed?.decision === 'string') return JSON.stringify(parsed);
3051
+ return candidate;
3052
+ } catch {}
3053
+ }
3054
+ }
3055
+ const jsonLines = text.split('\n').map((line) => line.trim()).filter(Boolean);
3056
+ for (let i = jsonLines.length - 1; i >= 0; i -= 1) {
3057
+ const candidate = jsonLines[i];
3058
+ if (!(candidate.startsWith('{') && candidate.endsWith('}'))) continue;
3059
+ try {
3060
+ JSON.parse(candidate);
3061
+ return candidate;
3062
+ } catch {}
3063
+ }
3064
+ return text;
3065
+ }
3066
+
3067
+ function normalizeResult(value) {
3068
+ return String(value || '').replace(/\s+/g, ' ').trim();
3069
+ }
3070
+
3071
+ function sanitizeHookSessionId(value) {
3072
+ const raw = String(value || '').trim();
3073
+ if (!raw) return `atel-hook-${Date.now()}`;
3074
+ const cleaned = raw.replace(/[^a-zA-Z0-9._:-]+/g, '-').replace(/^-+|-+$/g, '');
3075
+ return (cleaned || `atel-hook-${Date.now()}`).slice(0, 120);
3076
+ }
3077
+
3078
+ function isOpenClawAgentInvocation(cmd, args = []) {
3079
+ const argv = [String(cmd || ''), ...args.map((v) => String(v || ''))];
3080
+ if (argv[0] === 'openclaw') return argv[1] === 'agent';
3081
+ if (argv[0] === 'npx') return argv[1] === 'openclaw' && argv[2] === 'agent';
3082
+ if (argv[0] === 'node') return argv.includes('agent') && argv.some((v) => /openclaw/i.test(v));
3083
+ return false;
3084
+ }
3085
+
3086
+ function prepareHookInvocation(cmd, args = [], hookKey, timeoutSeconds) {
3087
+ const nextArgs = [...args];
3088
+ if (!isOpenClawAgentInvocation(cmd, nextArgs)) return { cmd, args: nextArgs };
3089
+
3090
+ if (!nextArgs.includes('--json')) {
3091
+ const messageIndex = nextArgs.lastIndexOf('-m');
3092
+ const insertAt = messageIndex >= 0 ? messageIndex : nextArgs.length;
3093
+ nextArgs.splice(insertAt, 0, '--json');
3094
+ }
3095
+ if (!nextArgs.includes('--session-id')) {
3096
+ const messageIndex = nextArgs.lastIndexOf('-m');
3097
+ const insertAt = messageIndex >= 0 ? messageIndex : nextArgs.length;
3098
+ nextArgs.splice(insertAt, 0, '--session-id', sanitizeHookSessionId(hookKey));
3099
+ }
3100
+ if (!nextArgs.includes('--timeout')) {
3101
+ const messageIndex = nextArgs.lastIndexOf('-m');
3102
+ const insertAt = messageIndex >= 0 ? messageIndex : nextArgs.length;
3103
+ nextArgs.splice(insertAt, 0, '--timeout', String(timeoutSeconds));
3104
+ }
3105
+ if (!nextArgs.includes('--thinking')) {
3106
+ const messageIndex = nextArgs.lastIndexOf('-m');
3107
+ const insertAt = messageIndex >= 0 ? messageIndex : nextArgs.length;
3108
+ nextArgs.splice(insertAt, 0, '--thinking', 'minimal');
3109
+ }
3110
+ return { cmd, args: nextArgs };
3111
+ }
3112
+
3113
+ function buildLocalAgentActionFromStdout(eventType, payload, stdout) {
3114
+ const cleaned = normalizeLocalAgentStdout(stdout);
3115
+ if (!cleaned) return { ok: false, error: 'empty_local_agent_stdout' };
3116
+
3117
+ if (eventType === 'milestone_submitted') {
3118
+ try {
3119
+ const parsed = JSON.parse(cleaned);
3120
+ return buildAgentCallbackAction(eventType, payload, parsed);
3121
+ } catch {
3122
+ const lowered = cleaned.toLowerCase();
3123
+ if (lowered.startsWith('pass') || cleaned.includes('通过')) {
3124
+ return buildAgentCallbackAction(eventType, payload, { decision: 'pass', summary: cleaned });
3125
+ }
3126
+ if (lowered.startsWith('reject') || cleaned.includes('拒绝')) {
3127
+ return buildAgentCallbackAction(eventType, payload, { decision: 'reject', reason: cleaned, summary: cleaned });
3128
+ }
3129
+ return { ok: false, error: 'invalid_local_review_stdout' };
3130
+ }
3131
+ }
3132
+
3133
+ if (['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected'].includes(eventType)) {
3134
+ try {
3135
+ const parsed = JSON.parse(cleaned);
3136
+ return buildAgentCallbackAction(eventType, payload, parsed);
3137
+ } catch {
3138
+ return buildAgentCallbackAction(eventType, payload, { result: cleaned, summary: cleaned });
3139
+ }
3140
+ }
3141
+
3142
+ return buildAgentCallbackAction(eventType, payload, { result: cleaned, summary: cleaned });
3143
+ }
3144
+
3145
+ function buildMilestoneHookRecoveryKey(eventType, payload = {}) {
3146
+ const orderId = String(payload?.orderId || '').trim();
3147
+ if (!orderId) return '';
3148
+ if (eventType === 'milestone_submitted') {
3149
+ const stage = Number.isFinite(Number(payload?.milestoneIndex)) ? Number(payload.milestoneIndex) : 0;
3150
+ const submitCount = Number.isFinite(Number(payload?.submitCount)) ? Number(payload.submitCount) : 0;
3151
+ return `stage:${orderId}:requester:${stage}:${submitCount}`;
3152
+ }
3153
+ if (['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected'].includes(eventType)) {
3154
+ const stage = eventType === 'milestone_plan_confirmed'
3155
+ ? (Number.isFinite(Number(payload?.milestoneIndex)) ? Number(payload.milestoneIndex) : 0)
3156
+ : (Number.isFinite(Number(payload?.currentMilestone)) ? Number(payload.currentMilestone) : Number.isFinite(Number(payload?.milestoneIndex)) ? Number(payload.milestoneIndex) : 0);
3157
+ return `stage:${orderId}:executor:${stage}`;
3158
+ }
3159
+ return '';
3160
+ }
3161
+
2737
3162
  async function runGatewayAgentTask(eventType, dedupeKey, promptText, cwd, payload) {
2738
3163
  const gw = discoverGateway();
2739
3164
  const cfg = loadOpenClawConfig();
@@ -2744,7 +3169,9 @@ ${callbackFailed}
2744
3169
  }
2745
3170
 
2746
3171
  const callbackUrl = `http://127.0.0.1:${p}/atel/v1/agent-callback`;
2747
- const taskPrompt = buildGatewayCallbackPrompt(eventType, promptText, callbackUrl, dedupeKey, cwd);
3172
+ const safePrompt = sanitizeAgentPrompt(promptText, { eventType, dedupeKey });
3173
+ if (!safePrompt) return { ok: false, error: 'empty_agent_prompt' };
3174
+ const taskPrompt = buildGatewayCallbackPrompt(eventType, safePrompt, callbackUrl, dedupeKey, cwd, payload);
2748
3175
  const timeoutMs = 10 * 60 * 1000;
2749
3176
 
2750
3177
  return await new Promise(async (resolve) => {
@@ -2810,9 +3237,11 @@ ${callbackFailed}
2810
3237
  }
2811
3238
 
2812
3239
  function queueAgentHook(eventType, dedupeKey, promptText, cwd, payload = {}, options = {}) {
2813
- if (!detectedAgentCmd || !promptText) return false;
3240
+ if (!detectedAgentCmd) return false;
3241
+ const safePrompt = sanitizeAgentPrompt(promptText, { eventType, dedupeKey });
3242
+ if (!safePrompt) return false;
2814
3243
  const parsedCmd = detectedAgentCmd.trim().split(/\s+/);
2815
- parsedCmd.push(promptText);
3244
+ parsedCmd.push(safePrompt);
2816
3245
  const recoveryKey = options.recoveryKey || '';
2817
3246
  if (recoveryKey) {
2818
3247
  if (activeRecoveryKeys.has(recoveryKey)) return false;
@@ -2842,6 +3271,8 @@ ${callbackFailed}
2842
3271
  const requesterDid = order?.requesterDid || order?.RequesterDID || '';
2843
3272
  const executorDid = order?.executorDid || order?.ExecutorDID || '';
2844
3273
  const orderStatus = order?.status || order?.Status || '';
3274
+ const orderDescription = order?.description || order?.Description || order?.taskRequest?.description || order?.TaskRequest?.description || '';
3275
+ const chain = order?.chain || order?.Chain || '';
2845
3276
 
2846
3277
  if (['cancelled', 'settled', 'rejected', 'expired'].includes(orderStatus)) {
2847
3278
  untrackOrder(orderId);
@@ -2850,7 +3281,7 @@ ${callbackFailed}
2850
3281
 
2851
3282
  if (orderStatus === 'milestone_review') {
2852
3283
  const approveAction = { type: 'cli', action: 'approve_plan', command: ['atel', 'milestone-feedback', orderId, '--approve'] };
2853
- const result = await executeRecommendedActionDirect('order_accepted', approveAction, process.cwd(), `reconcile:${orderId}:approve_plan`);
3284
+ const result = await executeRecommendedActionDirect('order_accepted', approveAction, getAtelWorkspaceRoot(), `reconcile:${orderId}:approve_plan`);
2854
3285
  log({ event: 'trade_reconcile_plan', orderId, ok: result.ok, role: requesterDid === id.did ? 'requester' : 'executor' });
2855
3286
  return;
2856
3287
  }
@@ -2863,6 +3294,18 @@ ${callbackFailed}
2863
3294
  if (executorDid === id.did && ms.phase === 'waiting_executor_submission') {
2864
3295
  const currentIndex = Number.isFinite(ms.currentMilestone) ? ms.currentMilestone : 0;
2865
3296
  const currentMilestone = (ms.milestones || []).find(m => m.index === currentIndex) || {};
3297
+ const previousApprovedOutputs = summarizeApprovedMilestones(ms.milestones || [], currentIndex);
3298
+ const workspace = getOrderWorkspace(orderId, {
3299
+ chain,
3300
+ role: 'executor',
3301
+ status: orderStatus,
3302
+ phase: ms.phase,
3303
+ currentMilestone: currentIndex,
3304
+ milestoneTitle: currentMilestone.title || '',
3305
+ orderDescription,
3306
+ milestoneObjective: currentMilestone.title || '',
3307
+ previousApprovedOutputs,
3308
+ });
2866
3309
  const eventType = currentIndex === 0 ? 'milestone_plan_confirmed' : 'milestone_verified';
2867
3310
  const payload = currentIndex === 0
2868
3311
  ? {
@@ -2870,7 +3313,8 @@ ${callbackFailed}
2870
3313
  milestoneIndex: 0,
2871
3314
  totalMilestones: ms.totalMilestones || 5,
2872
3315
  milestoneDescription: currentMilestone.title || '',
2873
- orderDescription: '',
3316
+ orderDescription,
3317
+ previousApprovedOutputs,
2874
3318
  }
2875
3319
  : {
2876
3320
  orderId,
@@ -2879,30 +3323,47 @@ ${callbackFailed}
2879
3323
  totalMilestones: ms.totalMilestones || 5,
2880
3324
  allComplete: false,
2881
3325
  nextMilestoneDescription: currentMilestone.title || '',
2882
- orderDescription: '',
3326
+ orderDescription,
3327
+ previousApprovedOutputs,
2883
3328
  };
2884
3329
  const promptText = currentIndex === 0
2885
- ? `你是ATEL接单方Agent。双方已确认方案,开始执行。\n当前里程碑 M0:${currentMilestone.title || ''}\n请认真完成这个里程碑,并通过回调返回最终交付内容。`
2886
- : `你是ATEL接单方Agent。M${currentIndex - 1} 已通过审核。\n下一个里程碑 M${currentIndex}:${currentMilestone.title || ''}\n请认真完成这个里程碑,并通过回调返回最终交付内容。`;
2887
- const recoveryKey = `reconcile:${orderId}:executor:${currentIndex}`;
2888
- const queued = queueAgentHook(eventType, recoveryKey, promptText, process.cwd(), payload, { recoveryKey });
2889
- if (queued) log({ event: 'trade_reconcile_executor', orderId, currentMilestone: currentIndex, recoveryKey });
3330
+ ? `你是ATEL接单方Agent。双方已确认方案,开始执行。\n订单原始要求:${orderDescription || '未提供'}\n当前里程碑 M0:${currentMilestone.title || ''}\n请只围绕这个订单要求完成当前里程碑,并通过回调返回最终交付内容。`
3331
+ : `你是ATEL接单方Agent。M${currentIndex - 1} 已通过审核。\n订单原始要求:${orderDescription || '未提供'}\n下一个里程碑 M${currentIndex}:${currentMilestone.title || ''}\n前面已通过的阶段结果如下:\n${previousApprovedOutputs || '无'}\n\n请严格基于这些已通过结果推进当前里程碑,不要自行假设缺失材料,也不要读取本地共享文件来补上下文。完成后通过回调返回最终交付内容。`;
3332
+ const recoveryKey = buildMilestoneHookRecoveryKey(eventType, payload);
3333
+ log({ event: 'trade_reconcile_executor', orderId, currentMilestone: currentIndex, recoveryKey });
3334
+ const queued = queueAgentHook(eventType, recoveryKey, promptText, workspace.dir, payload, { recoveryKey });
3335
+ if (queued) log({ event: 'trade_reconcile_executor_queued', orderId, currentMilestone: currentIndex, recoveryKey });
2890
3336
  return;
2891
3337
  }
2892
3338
 
2893
3339
  if (requesterDid === id.did && ms.phase === 'waiting_requester_verification') {
2894
3340
  const submittedMilestone = (ms.milestones || []).find(m => m.status === 'submitted');
2895
3341
  if (!submittedMilestone) return;
3342
+ const previousApprovedOutputs = summarizeApprovedMilestones(ms.milestones || [], submittedMilestone.index);
3343
+ const workspace = getOrderWorkspace(orderId, {
3344
+ chain,
3345
+ role: 'requester',
3346
+ status: orderStatus,
3347
+ phase: ms.phase,
3348
+ currentMilestone: submittedMilestone.index,
3349
+ milestoneTitle: submittedMilestone.title || '',
3350
+ orderDescription,
3351
+ milestoneObjective: submittedMilestone.title || '',
3352
+ resultSummary: submittedMilestone.resultSummary || '',
3353
+ previousApprovedOutputs,
3354
+ });
2896
3355
  const payload = {
2897
3356
  orderId,
2898
3357
  milestoneIndex: submittedMilestone.index,
2899
3358
  milestoneDescription: submittedMilestone.title || '',
2900
3359
  resultSummary: submittedMilestone.resultSummary || '',
2901
3360
  submitCount: submittedMilestone.submitCount || 0,
3361
+ orderDescription,
3362
+ previousApprovedOutputs,
2902
3363
  };
2903
- const promptText = `你是ATEL发单方Agent,需要审核执行方提交的工作。\n里程碑目标:${submittedMilestone.title || ''}\n提交内容:${submittedMilestone.resultSummary || ''}\n请审慎决定通过还是拒绝,并通过回调返回 decision=pass 或 decision=reject。`;
2904
- const recoveryKey = `reconcile:${orderId}:requester:${submittedMilestone.index}:${submittedMilestone.submitCount || 0}`;
2905
- const queued = queueAgentHook('milestone_submitted', recoveryKey, promptText, process.cwd(), payload, { recoveryKey });
3364
+ const promptText = `你是ATEL发单方Agent,需要审核执行方提交的工作。\n订单原始要求:${orderDescription || '未提供'}\n里程碑目标:${submittedMilestone.title || ''}\n前面已通过的阶段结果如下:\n${previousApprovedOutputs || '无'}\n提交内容:${submittedMilestone.resultSummary || ''}\n请只按该订单要求和前序已通过结果审慎决定通过还是拒绝,并通过回调返回 decision=pass 或 decision=reject。`;
3365
+ const recoveryKey = buildMilestoneHookRecoveryKey('milestone_submitted', payload);
3366
+ const queued = queueAgentHook('milestone_submitted', recoveryKey, promptText, workspace.dir, payload, { recoveryKey });
2906
3367
  if (queued) log({ event: 'trade_reconcile_requester', orderId, milestoneIndex: submittedMilestone.index, recoveryKey });
2907
3368
  }
2908
3369
  }
@@ -3005,7 +3466,20 @@ ${callbackFailed}
3005
3466
  });
3006
3467
 
3007
3468
  const dedupeKey = body.dedupeKey || `${event}:${body.orderId || payload.orderId || ''}`;
3008
- const cwd = process.cwd();
3469
+ const orderIdForCwd = body.orderId || payload.orderId || '';
3470
+ const workspace = getOrderWorkspace(orderIdForCwd, {
3471
+ chain: payload.chain || body.chain || '',
3472
+ role: payload.executorDid === id.did ? 'executor' : (payload.requesterDid === id.did ? 'requester' : ''),
3473
+ status: payload.orderStatus || body.orderStatus || '',
3474
+ phase: payload.phase || body.phase || '',
3475
+ currentMilestone: payload.currentMilestone ?? payload.milestoneIndex ?? '',
3476
+ milestoneTitle: payload.milestoneDescription || payload.nextMilestoneDescription || '',
3477
+ orderDescription: payload.orderDescription || payload.description || '',
3478
+ milestoneObjective: payload.milestoneDescription || payload.nextMilestoneDescription || '',
3479
+ resultSummary: payload.resultSummary || '',
3480
+ });
3481
+ const hookCwd = workspace.dir;
3482
+ const atelCwd = getAtelWorkspaceRoot();
3009
3483
 
3010
3484
  // 3. Policy mode: auto-execute deterministic operations (not thinking/work)
3011
3485
  const currentPolicy = loadPolicy();
@@ -3070,7 +3544,7 @@ ${callbackFailed}
3070
3544
  let directExecutionSucceeded = false;
3071
3545
  const directActions = getDirectExecutableActions(event, recommendedActions);
3072
3546
  for (const action of directActions) {
3073
- const result = await executeRecommendedActionDirect(event, action, cwd, dedupeKey);
3547
+ const result = await executeRecommendedActionDirect(event, action, atelCwd, dedupeKey);
3074
3548
  if (result.ok) directExecutionSucceeded = true;
3075
3549
  }
3076
3550
 
@@ -3081,14 +3555,24 @@ ${callbackFailed}
3081
3555
  const autoTriggerEvents = ['order_accepted', 'milestone_plan_confirmed', 'milestone_submitted', 'milestone_verified', 'milestone_rejected'];
3082
3556
  const hasGatewayAction = Array.isArray(recommendedActions) && recommendedActions.some((action) => Array.isArray(action?.command) && action.command[0] === 'atel');
3083
3557
  if (agentCmd && prompt && autoTriggerEvents.includes(event) && !shouldSkipAgentHook(event, directExecutionSucceeded)) {
3558
+ const needsActionablePayload = event !== 'milestone_submitted';
3559
+ if (needsActionablePayload && !hasGatewayAction) {
3560
+ log({ event: 'agent_hook_skip_no_action', eventType: event, dedupeKey, reason: 'informational_only_payload' });
3561
+ res.json({ status: 'received', eventId, eventType: event });
3562
+ return;
3563
+ }
3084
3564
  if (shouldUseGatewaySession(event) && !hasGatewayAction) {
3085
3565
  log({ event: 'agent_hook_skip_no_action', eventType: event, dedupeKey, reason: 'informational_only_payload' });
3086
3566
  res.json({ status: 'received', eventId, eventType: event });
3087
3567
  return;
3088
3568
  }
3089
3569
  // Add working directory context so agent runs atel commands in the right place
3090
- const cwdNote = `\n\n重要:所有 atel 命令必须在目录 ${cwd} 下执行(cd ${cwd} && atel ...)。`;
3091
- const enrichedPrompt = prompt + cwdNote;
3570
+ const cwdNote = `\n\n重要:OpenClaw 的分析工作目录是 ${hookCwd}。所有 atel 命令必须在目录 ${atelCwd} 下执行(cd ${atelCwd} && atel ...)。`;
3571
+ const enrichedPrompt = sanitizeAgentPrompt(prompt + cwdNote, { eventType: event, dedupeKey });
3572
+ if (!enrichedPrompt) {
3573
+ res.json({ status: 'received', eventId, eventType: event, skipped: true });
3574
+ return;
3575
+ }
3092
3576
 
3093
3577
  // Skip if already triggered for this dedupeKey
3094
3578
  if (processedEvents.has('hook:' + dedupeKey)) {
@@ -3096,13 +3580,17 @@ ${callbackFailed}
3096
3580
  } else {
3097
3581
  processedEvents.add('hook:' + dedupeKey);
3098
3582
 
3099
- // Build argv array
3100
- const parsedCmd = agentCmd.trim().split(/\s+/);
3101
- parsedCmd.push(enrichedPrompt);
3102
-
3103
- // Queue the hook (serialize to avoid session lock conflicts)
3104
- hookQueue.push({ event, dedupeKey, cmd: parsedCmd[0], args: parsedCmd.slice(1), cwd, payload, recoveryKey: '' });
3105
- if (!hookBusy) processHookQueue();
3583
+ const queued = queueAgentHook(
3584
+ event,
3585
+ dedupeKey,
3586
+ enrichedPrompt,
3587
+ hookCwd,
3588
+ payload,
3589
+ { recoveryKey: buildMilestoneHookRecoveryKey(event, payload) },
3590
+ );
3591
+ if (!queued) {
3592
+ log({ event: 'agent_cmd_dedup_recovery_key', eventType: event, dedupeKey });
3593
+ }
3106
3594
  }
3107
3595
  }
3108
3596
 
@@ -3131,11 +3619,18 @@ ${callbackFailed}
3131
3619
  return;
3132
3620
  }
3133
3621
  log({ event: 'agent_session_spawn_error', eventType: hookEvent, dedupeKey: hookKey, error: gatewayResult.error, fallback: 'cli' });
3622
+ spawnArgs[spawnArgs.length - 1] = buildLocalAgentPrompt(hookEvent, promptArg);
3623
+ } else if (spawnArgs.length > 0) {
3624
+ const promptArg = spawnArgs[spawnArgs.length - 1] || '';
3625
+ spawnArgs[spawnArgs.length - 1] = buildLocalAgentPrompt(hookEvent, promptArg);
3134
3626
  }
3135
3627
 
3136
3628
  const MAX_ATTEMPTS = 5;
3137
- const runHook = (attempt) => {
3138
- execFile(spawnCmd, spawnArgs, { timeout: 600000, cwd: hookCwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
3629
+ const isMilestoneHook = ['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected', 'milestone_submitted'].includes(hookEvent);
3630
+ const localHookTimeoutMs = isMilestoneHook ? 180000 : 600000;
3631
+ const preparedInvocation = prepareHookInvocation(spawnCmd, spawnArgs, hookKey, Math.ceil(localHookTimeoutMs / 1000));
3632
+ const runHook = (attempt, invocation = preparedInvocation) => {
3633
+ execFile(invocation.cmd, invocation.args, { timeout: localHookTimeoutMs, cwd: hookCwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
3139
3634
  const errMsg = (err?.message || '') + (stderr || '');
3140
3635
  const isSessionLock = errMsg.includes('session file locked') || errMsg.includes('session locked');
3141
3636
  const isNetworkError = err && (err.killed || err.code === 'ETIMEDOUT' || err.code === 'ECONNRESET');
@@ -3151,8 +3646,40 @@ ${callbackFailed}
3151
3646
  } else if (err) {
3152
3647
  log({ event: 'agent_cmd_error', eventType: hookEvent, error: err.message, stderr: (stderr || '').substring(0, 200) });
3153
3648
  finishHook();
3649
+ } else if (isKnownUpstreamModelInputError(stdout)) {
3650
+ log({
3651
+ event: 'agent_cmd_upstream_input_error',
3652
+ eventType: hookEvent,
3653
+ dedupeKey: hookKey,
3654
+ note: 'suppressed known upstream model input-length error',
3655
+ });
3656
+ finishHook();
3154
3657
  } else {
3155
- log({ event: 'agent_cmd_done', eventType: hookEvent, stdout: (stdout || '').substring(0, 300) });
3658
+ const localAction = buildLocalAgentActionFromStdout(hookEvent, hookPayload || {}, stdout);
3659
+ if (!localAction.ok && localAction.error === 'empty_local_agent_stdout' && invocation.args.includes('--json')) {
3660
+ const retryArgs = invocation.args.filter((arg) => arg !== '--json');
3661
+ log({ event: 'agent_cmd_retry_without_json', eventType: hookEvent, dedupeKey: hookKey });
3662
+ setTimeout(() => runHook(attempt + 1, { ...invocation, args: retryArgs }), 1000);
3663
+ return;
3664
+ }
3665
+ if (localAction.ok && !localAction.skipped) {
3666
+ executeRecommendedActionDirect(hookEvent, localAction.action, hookCwd || process.cwd(), hookKey)
3667
+ .then((execResult) => {
3668
+ if (execResult.ok) {
3669
+ log({ event: 'agent_cmd_done', eventType: hookEvent, mode: 'local_stdout_action', dedupeKey: hookKey, stdout: summarizeAgentOutput(stdout, 200) });
3670
+ } else {
3671
+ log({ event: 'agent_cmd_local_action_error', eventType: hookEvent, dedupeKey: hookKey, error: execResult.error || 'local_action_failed', stdout: summarizeAgentOutput(stdout, 200) });
3672
+ }
3673
+ finishHook();
3674
+ })
3675
+ .catch((e) => {
3676
+ log({ event: 'agent_cmd_local_action_error', eventType: hookEvent, dedupeKey: hookKey, error: e.message || 'local_action_failed', stdout: summarizeAgentOutput(stdout, 200) });
3677
+ finishHook();
3678
+ });
3679
+ return;
3680
+ }
3681
+
3682
+ log({ event: 'agent_cmd_done', eventType: hookEvent, stdout: summarizeAgentOutput(stdout, 300) });
3156
3683
  finishHook();
3157
3684
  }
3158
3685
  });
@@ -18,13 +18,10 @@ export function shouldSkipAgentHook(eventType, directExecutionSucceeded) {
18
18
  }
19
19
 
20
20
  export function shouldUseGatewaySession(eventType) {
21
- return [
22
- 'p2p_task',
23
- 'milestone_plan_confirmed',
24
- 'milestone_submitted',
25
- 'milestone_verified',
26
- 'milestone_rejected',
27
- ].includes(eventType);
21
+ // Keep gateway sub-sessions only for explicit P2P task execution.
22
+ // Milestone automation must not depend on gateway callback health; use the
23
+ // local structured fallback so order progression cannot stall on subagent I/O.
24
+ return eventType === 'p2p_task';
28
25
  }
29
26
 
30
27
  export function normalizeGatewayBind(bind) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lawrenceliang-btc/atel-sdk",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
4
  "description": "ATEL Protocol SDK - Agent Trust & Exchange Layer",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,6 +18,7 @@ ATEL 负责:
18
18
  - relay / endpoint / inbox
19
19
  - paid order 状态机
20
20
  - 通知与回调
21
+ - paid order 的链字段与链上记录
21
22
 
22
23
  OpenClaw 负责:
23
24
 
@@ -123,6 +124,32 @@ echo "========================================="
123
124
  - `atel start` 会启动 ATEL 本地 endpoint、relay 轮询、通知、回调处理
124
125
  - 具体“怎么思考、怎么写内容、怎么调用工具”由 OpenClaw 完成
125
126
  - 不要把 ATEL 理解成内置了一个通用 LLM 执行器
127
+ - paid order 目前按正式支持链处理:
128
+ - `Base`
129
+ - `BSC`
130
+ - **paid order 的唯一链真相源是 `order.chain`**
131
+ - 订单在哪条链,你就按哪条链理解:
132
+ - smart wallet
133
+ - USDC 余额
134
+ - gas
135
+ - escrow
136
+ - chain-records
137
+
138
+ ### 双链使用规则
139
+
140
+ 处理 paid order 时,必须遵守:
141
+
142
+ 1. 不要默认所有订单都在 Base
143
+ 2. 先用 `atel order-info <orderId>` 或 `atel milestone-status <orderId>` 看 `chain`
144
+ 3. 后续所有跟链有关的判断都跟 `order.chain`
145
+ 4. 如果订单是 `bsc`,就不要再按 `base` 钱包、`base` gas、`base` 浏览器去理解
146
+
147
+ 常见含义:
148
+
149
+ - `base`:
150
+ - Base 主网 USDC / gas / chain record
151
+ - `bsc`:
152
+ - BSC 主网 USDC / gas / chain record
126
153
 
127
154
  ---
128
155
 
@@ -137,6 +164,16 @@ atel order <executor-DID> general <金额> --desc "任务描述"
137
164
 
138
165
  发单前确保钱包有 USDC(`atel balance` 查看)。
139
166
 
167
+ ⚠️ 双链说明:
168
+
169
+ - paid order 不是默认只有 Base
170
+ - 发单时要明确知道自己准备用哪条链完成交易
171
+ - 后续 accept / escrow / submit / verify / settle 都会跟该订单的 `chain`
172
+ - 如果要做双链 paid order,发单方和接单方都需要在对应链准备:
173
+ - smart wallet
174
+ - USDC
175
+ - gas
176
+
140
177
  ### 接单(Executor)
141
178
 
142
179
  收到新订单通知时,人类确认后:
@@ -154,6 +191,14 @@ atel orders executor # 只看接的单
154
191
  atel order-info <orderId> # 查看某个订单详情
155
192
  ```
156
193
 
194
+ 看订单时一定注意:
195
+
196
+ - `order.chain`
197
+ - `escrow.chain`
198
+ - `chain-records`
199
+
200
+ 这几个字段会直接决定后续链上动作在哪条链执行。
201
+
157
202
  ### 一键下单(搜索 + 下单 + 等待)
158
203
 
159
204
  ```bash
@@ -178,16 +223,27 @@ atel trade-task <capability> "任务描述" --budget 5
178
223
 
179
224
  ```bash
180
225
  cd ~/atel-workspace
226
+ atel order-info <orderId>
181
227
  atel milestone-status <orderId>
182
228
  atel milestone-feedback <orderId> --approve
183
229
  ```
184
230
 
231
+ 注意:
232
+
233
+ - 先确认 `order.chain`
234
+ - 不要把 paid order 默认当成 Base 单
235
+
185
236
  **`milestone_plan_confirmed` — 方案确认了(你是接单方):**
186
237
  - prompt 里有里程碑描述,用你的 AI 能力完成工作
187
238
  ```bash
188
239
  cd ~/atel-workspace && atel milestone-submit <orderId> <index> --result '<你的交付内容>'
189
240
  ```
190
241
 
242
+ 提交前要明确:
243
+
244
+ - 当前订单在哪条链
245
+ - 后续 anchor / settle / chain-records 都会落在这条链
246
+
191
247
  **`milestone_submitted` — 对方提交了(你是发单方):**
192
248
  - prompt 里有里程碑目标和提交内容,认真审核
193
249
  - 质量达标就通过,不达标就写清楚具体原因
@@ -221,6 +277,12 @@ cd ~/atel-workspace && atel balance
221
277
  cd ~/atel-workspace && atel chain-records <orderId>
222
278
  ```
223
279
 
280
+ 结算后检查时,不要只看 Base:
281
+
282
+ - `atel balance` 会显示链上钱包情况
283
+ - `atel chain-records <orderId>` 要确认该订单对应链上的记录
284
+ - 如果这是 `bsc` 单,就按 `bsc` 的链上记录理解结果
285
+
224
286
  ---
225
287
 
226
288
  ## 三、P2P 与消息
@@ -244,6 +306,16 @@ ATEL 有两种轻量协作方式,不要混淆:
244
306
  如果用户想“直接让对方做一个轻任务”,用 `atel task`。
245
307
  如果用户想“带付款、验收、结算”,用 `atel order`。
246
308
 
309
+ 补充:
310
+
311
+ - `atel task` 和 `atel send` 不走 paid order 双链结算流
312
+ - `atel order` 才会进入:
313
+ - escrow
314
+ - milestone
315
+ - chain-records
316
+ - dispute
317
+ - 所以只有 `atel order` 需要严格理解 `Base / BSC`
318
+
247
319
  ---
248
320
 
249
321
  ## 四、社交通信
@@ -324,14 +396,22 @@ atel offer-close <offerId>
324
396
  ## 五、账户管理
325
397
 
326
398
  ```bash
327
- atel balance # 查余额
328
- atel deposit 10 crypto_base # 充值 10 USDC(Base 链)
329
- atel withdraw 5 crypto_base <钱包地址> # 提现
330
- atel transactions # 交易记录
399
+ atel balance # 查余额(会看到 Base / BSC)
400
+ atel deposit 10 crypto_base # 充值 10 USDC(Base
401
+ atel deposit 10 crypto_bsc # 充值 10 USDC(BSC)
402
+ atel withdraw 5 crypto_base <钱包地址> # 从 Base 提现
403
+ atel withdraw 5 crypto_bsc <钱包地址> # 从 BSC 提现
404
+ atel transactions # 交易记录
331
405
  ```
332
406
 
333
407
  支持的充值渠道:`crypto_solana`、`crypto_base`、`crypto_bsc`、`stripe`、`alipay`、`manual`
334
408
 
409
+ 注意:
410
+
411
+ - 双链 paid order 场景下,余额检查不能只看 Base
412
+ - 你要确认订单实际在哪条链,再决定看哪条链的钱包与 USDC
413
+ - 如果订单是 `bsc`,就不要只用 `crypto_base` 的心智理解充值、提现和结算
414
+
335
415
  ---
336
416
 
337
417
  ## 六、信任与安全