zyndo 0.3.4 → 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/dist/agentLoop.d.ts +10 -0
- package/dist/commands/seller.d.ts +8 -0
- package/dist/commands/seller.js +142 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +10 -2
- package/dist/connection.d.ts +1 -0
- package/dist/connection.js +35 -0
- package/dist/harnessSelfCheck.d.ts +14 -0
- package/dist/harnessSelfCheck.js +165 -0
- package/dist/index.js +8 -0
- package/dist/init.js +29 -20
- package/dist/providers/claudeCode.js +48 -7
- package/dist/sellerDaemon.js +38 -1
- package/package.json +1 -1
package/dist/agentLoop.d.ts
CHANGED
|
@@ -10,5 +10,15 @@ export type AgentLoopResult = Readonly<{
|
|
|
10
10
|
paused: boolean;
|
|
11
11
|
timedOut?: boolean;
|
|
12
12
|
pendingQuestion?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Set when the underlying agent harness (codex, claude, generic binary)
|
|
15
|
+
* exited with a non-zero status and produced no usable output. The caller
|
|
16
|
+
* MUST NOT deliver `output` to the buyer in this case — it is a stderr
|
|
17
|
+
* dump, not a deliverable. Instead, send a human-readable info message
|
|
18
|
+
* and leave the task alone so the buyer can revise or dispute.
|
|
19
|
+
*/
|
|
20
|
+
harnessFailed?: boolean;
|
|
21
|
+
/** When harnessFailed is set, this is the short human-readable reason. */
|
|
22
|
+
harnessError?: string;
|
|
13
23
|
}>;
|
|
14
24
|
export declare function runAgentLoop(provider: LLMProvider, tools: ReadonlyArray<Tool>, initialMessage: string, opts: AgentLoopOptions): Promise<AgentLoopResult>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type SellerConfig } from '../config.js';
|
|
2
|
+
export type SellerCommandDeps = Readonly<{
|
|
3
|
+
loadConfig: () => SellerConfig;
|
|
4
|
+
httpFetch: typeof fetch;
|
|
5
|
+
stderr: (msg: string) => void;
|
|
6
|
+
stdout: (msg: string) => void;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function handleSellerCommand(args: ReadonlyArray<string>, deps?: SellerCommandDeps): Promise<void>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// zyndo seller — local CLI for managing seller ownership
|
|
3
|
+
//
|
|
4
|
+
// Subcommands:
|
|
5
|
+
// zyndo seller list
|
|
6
|
+
// GET /user/seller-agents — prints the user's slug ↔ agentId table
|
|
7
|
+
// so the operator can find orphaned reputation after a slug change.
|
|
8
|
+
//
|
|
9
|
+
// zyndo seller adopt <agentId>
|
|
10
|
+
// POST /user/seller-agents/adopt — rebinds the current seller.yaml
|
|
11
|
+
// slug to an existing agentId the user already owns. Used to reclaim
|
|
12
|
+
// reputation when the local slug drifts from what the broker persisted.
|
|
13
|
+
//
|
|
14
|
+
// zyndo seller retire <slug>
|
|
15
|
+
// POST /user/seller-agents/:slug/retire — marks a slug row retired so
|
|
16
|
+
// it stops being restored on connect and frees a cap slot.
|
|
17
|
+
//
|
|
18
|
+
// Auth: reads `api_key` from the local seller.yaml and passes it via
|
|
19
|
+
// `x-zyndo-api-key`. The /user/seller-agents/* routes accept API keys as
|
|
20
|
+
// a dashboard-less auth path (incident 2026-04-15).
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
import { loadSellerConfig } from '../config.js';
|
|
23
|
+
const DEFAULT_DEPS = {
|
|
24
|
+
loadConfig: () => loadSellerConfig(),
|
|
25
|
+
httpFetch: fetch,
|
|
26
|
+
stderr: (msg) => process.stderr.write(msg),
|
|
27
|
+
stdout: (msg) => process.stdout.write(msg)
|
|
28
|
+
};
|
|
29
|
+
export async function handleSellerCommand(args, deps = DEFAULT_DEPS) {
|
|
30
|
+
const [subcommand, ...rest] = args;
|
|
31
|
+
if (subcommand === undefined) {
|
|
32
|
+
printUsage(deps);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let config;
|
|
37
|
+
try {
|
|
38
|
+
config = deps.loadConfig();
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
deps.stderr(`Failed to load seller config: ${err.message}\n`);
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (config.apiKey.length === 0) {
|
|
46
|
+
deps.stderr('Missing api_key in seller.yaml. Add one from https://zyndo.ai/dashboard (API Keys tab).\n');
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const headers = {
|
|
51
|
+
'content-type': 'application/json',
|
|
52
|
+
'x-zyndo-api-key': config.apiKey
|
|
53
|
+
};
|
|
54
|
+
switch (subcommand) {
|
|
55
|
+
case 'list': {
|
|
56
|
+
const res = await deps.httpFetch(`${config.bridgeUrl}/user/seller-agents`, { headers });
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
deps.stderr(`List failed (${res.status}): ${await res.text().catch(() => '')}\n`);
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const body = (await res.json());
|
|
63
|
+
if (body.agents.length === 0) {
|
|
64
|
+
deps.stdout('No sellers registered under this account.\n');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
deps.stdout(`Sellers for this account (${body.agents.length}):\n\n`);
|
|
68
|
+
for (const a of body.agents) {
|
|
69
|
+
const status = a.retiredAt !== undefined ? '[retired]' : a.online ? '[online]' : '[offline]';
|
|
70
|
+
deps.stdout(` ${status} ${a.displayName}\n` +
|
|
71
|
+
` slug: ${a.sellerSlug}\n` +
|
|
72
|
+
` agentId: ${a.agentId}\n` +
|
|
73
|
+
` tasks: ${a.completedTaskCount} completed / ${a.lifetimeTaskCount} lifetime\n` +
|
|
74
|
+
` last seen ${a.lastSeenAt}\n\n`);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
case 'adopt': {
|
|
79
|
+
const agentId = rest[0];
|
|
80
|
+
if (agentId === undefined || agentId.length === 0) {
|
|
81
|
+
deps.stderr('Usage: zyndo seller adopt <agentId>\n');
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (config.id === undefined || config.id.length === 0) {
|
|
86
|
+
deps.stderr('Current seller.yaml has no `id:` slug. Add one and retry.\n');
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const res = await deps.httpFetch(`${config.bridgeUrl}/user/seller-agents/adopt`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers,
|
|
93
|
+
body: JSON.stringify({ sellerSlug: config.id, agentId })
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
const text = await res.text().catch(() => '');
|
|
97
|
+
deps.stderr(`Adopt failed (${res.status}): ${text}\n`);
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const body = (await res.json());
|
|
102
|
+
deps.stdout(`Adopted agentId ${body.agentId} under slug "${body.sellerSlug}" (display name: ${body.displayName}).\n`);
|
|
103
|
+
if (body.previousSlug !== undefined) {
|
|
104
|
+
deps.stdout(`Previous slug "${body.previousSlug}" was retired to free the binding.\n`);
|
|
105
|
+
}
|
|
106
|
+
deps.stdout('Run `zyndo serve` to resume as this seller. Reputation stays attached to the adopted agentId.\n');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
case 'retire': {
|
|
110
|
+
const slug = rest[0];
|
|
111
|
+
if (slug === undefined || slug.length === 0) {
|
|
112
|
+
deps.stderr('Usage: zyndo seller retire <slug>\n');
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const res = await deps.httpFetch(`${config.bridgeUrl}/user/seller-agents/${encodeURIComponent(slug)}/retire`, { method: 'POST', headers });
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
deps.stderr(`Retire failed (${res.status}): ${await res.text().catch(() => '')}\n`);
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const body = (await res.json());
|
|
123
|
+
if (body.alreadyRetired === true) {
|
|
124
|
+
deps.stdout(`Slug "${body.sellerSlug}" was already retired. No change.\n`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
deps.stdout(`Retired slug "${body.sellerSlug}". Its cap slot is freed.\n`);
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
default: {
|
|
132
|
+
printUsage(deps);
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function printUsage(deps) {
|
|
138
|
+
deps.stderr('Usage:\n' +
|
|
139
|
+
' zyndo seller list List seller agents on this account\n' +
|
|
140
|
+
' zyndo seller adopt <agentId> Rebind current seller.yaml slug to an existing agentId\n' +
|
|
141
|
+
' zyndo seller retire <slug> Retire a seller slug (frees a cap slot)\n');
|
|
142
|
+
}
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -44,7 +44,13 @@ export function loadSellerConfig(configPath) {
|
|
|
44
44
|
const name = requireString(data, 'name');
|
|
45
45
|
const description = requireString(data, 'description');
|
|
46
46
|
const provider = requireEnum(data, 'provider', ['anthropic', 'openai', 'ollama', 'claude-code']);
|
|
47
|
-
|
|
47
|
+
// Optional: when empty, the harness CLI uses its own native default.
|
|
48
|
+
// For codex: reads ~/.codex/config.toml `model` (auto-tracks whatever
|
|
49
|
+
// OpenAI currently supports on the seller's ChatGPT plan). For claude:
|
|
50
|
+
// uses Claude Code's built-in default. Incident 2026-04-14: a hardcoded
|
|
51
|
+
// `gpt-5-codex` override broke every task when OpenAI rotated the
|
|
52
|
+
// ChatGPT-edu allowlist to `gpt-5.3-codex`. Blank is the safest default.
|
|
53
|
+
const model = optionalString(data, 'model') ?? '';
|
|
48
54
|
const providerApiKey = optionalString(data, 'provider_api_key');
|
|
49
55
|
const workingDirectory = optionalString(data, 'working_directory') ?? process.cwd();
|
|
50
56
|
const systemPrompt = optionalString(data, 'system_prompt') ?? 'You are a helpful AI agent.';
|
|
@@ -89,6 +95,7 @@ export function loadSellerConfig(configPath) {
|
|
|
89
95
|
const claudeCodeTimeoutMs = typeof data.claude_code_timeout_ms === 'number' ? data.claude_code_timeout_ms : undefined;
|
|
90
96
|
const claudeCodeMaxBudgetUsd = typeof data.claude_code_max_budget_usd === 'number' ? data.claude_code_max_budget_usd : undefined;
|
|
91
97
|
const identityKeyPath = optionalString(data, 'identity_key_path');
|
|
98
|
+
const forceNewSlug = data.force_new_slug === true;
|
|
92
99
|
const skills = requireArray(data, 'skills').map((s) => {
|
|
93
100
|
const skill = s;
|
|
94
101
|
const id = requireString(skill, 'id');
|
|
@@ -137,7 +144,8 @@ export function loadSellerConfig(configPath) {
|
|
|
137
144
|
claudeCodeBinary,
|
|
138
145
|
claudeCodeTimeoutMs,
|
|
139
146
|
claudeCodeMaxBudgetUsd,
|
|
140
|
-
identityKeyPath
|
|
147
|
+
identityKeyPath,
|
|
148
|
+
forceNewSlug
|
|
141
149
|
};
|
|
142
150
|
}
|
|
143
151
|
// ---------------------------------------------------------------------------
|
package/dist/connection.d.ts
CHANGED
|
@@ -91,6 +91,7 @@ export declare function connect(bridgeUrl: string, apiKey: string, opts: {
|
|
|
91
91
|
categories?: ReadonlyArray<string>;
|
|
92
92
|
maxConcurrentTasks?: number;
|
|
93
93
|
sellerSlug?: string;
|
|
94
|
+
forceNewSlug?: boolean;
|
|
94
95
|
}): Promise<AgentSession>;
|
|
95
96
|
export declare function reconnect(session: AgentSession, apiKey: string, opts?: {
|
|
96
97
|
role?: 'buyer' | 'seller';
|
package/dist/connection.js
CHANGED
|
@@ -117,6 +117,8 @@ export async function connect(bridgeUrl, apiKey, opts) {
|
|
|
117
117
|
};
|
|
118
118
|
if (opts.sellerSlug !== undefined)
|
|
119
119
|
body.sellerSlug = opts.sellerSlug;
|
|
120
|
+
if (opts.forceNewSlug === true)
|
|
121
|
+
body.forceNewSlug = true;
|
|
120
122
|
const res = await fetch(`${bridgeUrl}/agent/connect`, {
|
|
121
123
|
method: 'POST',
|
|
122
124
|
headers,
|
|
@@ -124,6 +126,39 @@ export async function connect(bridgeUrl, apiKey, opts) {
|
|
|
124
126
|
});
|
|
125
127
|
if (!res.ok) {
|
|
126
128
|
const errBody = await res.text();
|
|
129
|
+
// Per-user seller cap: surface a human-readable hint so the operator
|
|
130
|
+
// can retire an existing slug instead of guessing why their daemon
|
|
131
|
+
// refused to start.
|
|
132
|
+
if (res.status === 409) {
|
|
133
|
+
let parsed;
|
|
134
|
+
try {
|
|
135
|
+
parsed = JSON.parse(errBody);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
parsed = undefined;
|
|
139
|
+
}
|
|
140
|
+
if (parsed !== undefined && parsed.error === 'seller_limit_reached') {
|
|
141
|
+
const existing = parsed.existing !== undefined && parsed.existing.length > 0
|
|
142
|
+
? parsed.existing.join(', ')
|
|
143
|
+
: '<none>';
|
|
144
|
+
throw new Error(`Connect failed: seller_limit_reached. This account already runs ${parsed.limit ?? '?'} sellers (${existing}). Retire one via the dashboard before launching another listing.`);
|
|
145
|
+
}
|
|
146
|
+
if (parsed !== undefined && parsed.error === 'possible_orphaned_seller') {
|
|
147
|
+
const candidates = parsed.candidates ?? [];
|
|
148
|
+
const lines = candidates
|
|
149
|
+
.map((c) => ` - agentId=${c.agentId} slug=${c.sellerSlug} last_seen=${c.lastSeenAt ?? '<unknown>'}`)
|
|
150
|
+
.join('\n');
|
|
151
|
+
throw new Error(`Connect failed: another seller on your account already uses the display name "${opts.name}".\n` +
|
|
152
|
+
`\n` +
|
|
153
|
+
`If it is the SAME seller you ran before, reclaim its reputation with:\n` +
|
|
154
|
+
` zyndo seller adopt <agentId>\n` +
|
|
155
|
+
`\n` +
|
|
156
|
+
`Candidates:\n${lines.length > 0 ? lines : ' (none reported by broker)'}\n` +
|
|
157
|
+
`\n` +
|
|
158
|
+
`If you really want a brand new empty seller under this display name, add this to seller.yaml:\n` +
|
|
159
|
+
` force_new_slug: true`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
127
162
|
throw new Error(`Connect failed (${res.status}): ${errBody}`);
|
|
128
163
|
}
|
|
129
164
|
const data = (await res.json());
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SellerConfig } from './config.js';
|
|
2
|
+
export type SelfCheckResult = Readonly<{
|
|
3
|
+
ok: true;
|
|
4
|
+
}> | Readonly<{
|
|
5
|
+
ok: false;
|
|
6
|
+
reason: string;
|
|
7
|
+
fix: string;
|
|
8
|
+
}>;
|
|
9
|
+
/**
|
|
10
|
+
* Spawn the harness with a trivial prompt and verify it completes successfully.
|
|
11
|
+
* This is the same command shape that `runClaudeCodeTask` uses, minus the
|
|
12
|
+
* system prompt / BOUND CONTRACT block so the probe stays fast (<30s).
|
|
13
|
+
*/
|
|
14
|
+
export declare function verifyHarnessReady(config: SellerConfig): Promise<SelfCheckResult>;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Harness self-check
|
|
3
|
+
//
|
|
4
|
+
// Runs a trivial dry-run spawn of the configured harness (claude / codex)
|
|
5
|
+
// at daemon startup, BEFORE the seller connects to the broker. Catches three
|
|
6
|
+
// failure modes that would otherwise burn a real buyer hire:
|
|
7
|
+
//
|
|
8
|
+
// 1. Seller pinned an explicit model that the account no longer allows
|
|
9
|
+
// (e.g. a stale `gpt-5-codex` after OpenAI rotated the ChatGPT allowlist
|
|
10
|
+
// to `gpt-5.3-codex`). Incident 2026-04-14.
|
|
11
|
+
// 2. Harness binary is missing, unauthenticated, or rate-limited.
|
|
12
|
+
// 3. Any other startup-time error the harness itself surfaces.
|
|
13
|
+
//
|
|
14
|
+
// The probe uses the SAME spawn path as runClaudeCodeTask (cross-spawn,
|
|
15
|
+
// stdin-piped prompt, -o tempfile for codex) so anything it reports is
|
|
16
|
+
// representative of what a real task would hit.
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
import spawn from 'cross-spawn';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { tmpdir } from 'node:os';
|
|
21
|
+
import { randomBytes } from 'node:crypto';
|
|
22
|
+
import { readFileSync, unlinkSync } from 'node:fs';
|
|
23
|
+
import { detectHarness } from './providers/claudeCode.js';
|
|
24
|
+
const PROBE_TIMEOUT_MS = 30_000;
|
|
25
|
+
const PROBE_PROMPT = 'Reply with exactly the two characters: ok';
|
|
26
|
+
/**
|
|
27
|
+
* Parse harness stderr for known failure patterns and return actionable fix
|
|
28
|
+
* text. Falls through to a generic "see stderr tail" message.
|
|
29
|
+
*/
|
|
30
|
+
function diagnoseFailure(stderr, harness, config) {
|
|
31
|
+
// Codex + Claude ChatGPT-account allowlist rejection. The exact string from
|
|
32
|
+
// Codex v0.116.0 is "The '<model>' model is not supported when using Codex
|
|
33
|
+
// with a ChatGPT account." — future Anthropic equivalents likely follow the
|
|
34
|
+
// same pattern, hence the broad regex.
|
|
35
|
+
if (/is not supported when using .* with a ChatGPT account/i.test(stderr)
|
|
36
|
+
|| /model .* is not (?:available|supported)/i.test(stderr)) {
|
|
37
|
+
const pinnedModel = config.model.length > 0 ? config.model : '(none)';
|
|
38
|
+
const fix = harness === 'codex'
|
|
39
|
+
? `Your seller.yaml pins model="${pinnedModel}" but your Codex account does not allow it.\n` +
|
|
40
|
+
` Fix: remove (or blank) the "model:" line in seller.yaml so Codex uses\n` +
|
|
41
|
+
` its own default from ~/.codex/config.toml. Alternatively, set\n` +
|
|
42
|
+
` OPENAI_API_KEY in your shell before "zyndo serve" to switch to API billing.`
|
|
43
|
+
: `Your seller.yaml pins model="${pinnedModel}" but your Claude account does not allow it.\n` +
|
|
44
|
+
` Fix: remove (or blank) the "model:" line in seller.yaml so Claude Code\n` +
|
|
45
|
+
` uses its own default model.`;
|
|
46
|
+
return { reason: 'Harness rejected the configured model (allowlist mismatch).', fix };
|
|
47
|
+
}
|
|
48
|
+
if (/401|unauthorized|not (?:logged in|authenticated)/i.test(stderr)) {
|
|
49
|
+
const cmd = harness === 'codex' ? 'codex login' : 'claude login';
|
|
50
|
+
return {
|
|
51
|
+
reason: 'Harness is not authenticated.',
|
|
52
|
+
fix: `Run "${cmd}" in this shell, then restart "zyndo serve".`
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (/rate.?limit/i.test(stderr)) {
|
|
56
|
+
return {
|
|
57
|
+
reason: 'Harness is rate-limited.',
|
|
58
|
+
fix: 'Wait a few minutes and restart "zyndo serve". If this keeps happening, upgrade your account plan.'
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (/command not found|ENOENT|spawn .* ENOENT/i.test(stderr)) {
|
|
62
|
+
return {
|
|
63
|
+
reason: 'Harness binary not found on PATH.',
|
|
64
|
+
fix: `Install the ${harness === 'codex' ? 'Codex' : 'Claude Code'} CLI and re-run "zyndo init" to update the binary path in seller.yaml.`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Generic fallback — show the tail so the operator can see the real error.
|
|
68
|
+
const tail = stderr.length > 1000 ? stderr.slice(-1000) : stderr;
|
|
69
|
+
return {
|
|
70
|
+
reason: 'Harness exited non-zero during startup probe.',
|
|
71
|
+
fix: `Stderr tail:\n${tail.trim()}`
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Spawn the harness with a trivial prompt and verify it completes successfully.
|
|
76
|
+
* This is the same command shape that `runClaudeCodeTask` uses, minus the
|
|
77
|
+
* system prompt / BOUND CONTRACT block so the probe stays fast (<30s).
|
|
78
|
+
*/
|
|
79
|
+
export async function verifyHarnessReady(config) {
|
|
80
|
+
const binary = config.claudeCodeBinary ?? 'claude';
|
|
81
|
+
const harness = config.harness ?? detectHarness(binary);
|
|
82
|
+
// `generic` harness has no prescribed args — we trust whatever shell the
|
|
83
|
+
// seller wired up. Skip the probe; there's nothing standard to test.
|
|
84
|
+
if (harness === 'generic') {
|
|
85
|
+
return { ok: true };
|
|
86
|
+
}
|
|
87
|
+
let outputFile;
|
|
88
|
+
const args = [];
|
|
89
|
+
if (harness === 'codex') {
|
|
90
|
+
outputFile = join(tmpdir(), `zyndo-probe-${Date.now()}-${randomBytes(4).toString('hex')}.txt`);
|
|
91
|
+
args.push('exec', '-');
|
|
92
|
+
if (config.model.length > 0)
|
|
93
|
+
args.push('-m', config.model);
|
|
94
|
+
args.push('--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check', '-C', config.workingDirectory, '--color', 'never', '-o', outputFile);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// claude
|
|
98
|
+
args.push('--print', '--output-format', 'json');
|
|
99
|
+
if (config.model.length > 0)
|
|
100
|
+
args.push('--model', config.model);
|
|
101
|
+
args.push('--add-dir', config.workingDirectory, '--permission-mode', 'bypassPermissions', '--no-session-persistence');
|
|
102
|
+
}
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timeoutHandle = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
106
|
+
const proc = spawn(binary, args, {
|
|
107
|
+
cwd: config.workingDirectory,
|
|
108
|
+
signal: controller.signal,
|
|
109
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
110
|
+
env: { ...process.env }
|
|
111
|
+
});
|
|
112
|
+
const stdoutChunks = [];
|
|
113
|
+
const stderrChunks = [];
|
|
114
|
+
proc.stdout.on('data', (c) => stdoutChunks.push(c));
|
|
115
|
+
proc.stderr.on('data', (c) => stderrChunks.push(c));
|
|
116
|
+
proc.on('error', (err) => {
|
|
117
|
+
clearTimeout(timeoutHandle);
|
|
118
|
+
if (outputFile !== undefined) {
|
|
119
|
+
try {
|
|
120
|
+
unlinkSync(outputFile);
|
|
121
|
+
}
|
|
122
|
+
catch { /* ignore */ }
|
|
123
|
+
}
|
|
124
|
+
if (err.name === 'AbortError') {
|
|
125
|
+
resolve({
|
|
126
|
+
ok: false,
|
|
127
|
+
reason: `Harness probe timed out after ${PROBE_TIMEOUT_MS / 1000}s.`,
|
|
128
|
+
fix: `The ${harness} binary hung without responding. Try running it manually once to confirm it works, then restart "zyndo serve".`
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
resolve({
|
|
133
|
+
ok: false,
|
|
134
|
+
reason: `Failed to spawn harness binary: ${err.message}`,
|
|
135
|
+
fix: `Check that "${binary}" is installed and on your PATH. Re-run "zyndo init" to update the binary path.`
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
proc.on('close', (code) => {
|
|
139
|
+
clearTimeout(timeoutHandle);
|
|
140
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf-8');
|
|
141
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf-8');
|
|
142
|
+
let probeOutput = stdout;
|
|
143
|
+
if (outputFile !== undefined) {
|
|
144
|
+
try {
|
|
145
|
+
probeOutput = readFileSync(outputFile, 'utf-8');
|
|
146
|
+
}
|
|
147
|
+
catch { /* use stdout */ }
|
|
148
|
+
try {
|
|
149
|
+
unlinkSync(outputFile);
|
|
150
|
+
}
|
|
151
|
+
catch { /* ignore */ }
|
|
152
|
+
}
|
|
153
|
+
if (code === 0 && probeOutput.trim().length > 0) {
|
|
154
|
+
resolve({ ok: true });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const diagnosis = diagnoseFailure(stderr, harness, config);
|
|
158
|
+
resolve({ ok: false, ...diagnosis });
|
|
159
|
+
});
|
|
160
|
+
// Feed the probe prompt and close stdin so the harness completes.
|
|
161
|
+
proc.stdin.on('error', () => { });
|
|
162
|
+
proc.stdin.write(PROBE_PROMPT);
|
|
163
|
+
proc.stdin.end();
|
|
164
|
+
});
|
|
165
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -70,6 +70,11 @@ async function main() {
|
|
|
70
70
|
await handleRefereeCommand(args.slice(1));
|
|
71
71
|
break;
|
|
72
72
|
}
|
|
73
|
+
case 'seller': {
|
|
74
|
+
const { handleSellerCommand } = await import('./commands/seller.js');
|
|
75
|
+
await handleSellerCommand(args.slice(1));
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
73
78
|
case 'version':
|
|
74
79
|
process.stdout.write(`zyndo ${getPackageVersion()}\n`);
|
|
75
80
|
break;
|
|
@@ -96,6 +101,9 @@ Usage:
|
|
|
96
101
|
zyndo cashout View earnings and cashout (opens dashboard)
|
|
97
102
|
zyndo connect [onboard|status] Stripe Connect setup (opens dashboard)
|
|
98
103
|
zyndo referee <enable|disable> <skillId> Toggle Kimi K2.5 referee sampling on a skill
|
|
104
|
+
zyndo seller list List seller agents on this account
|
|
105
|
+
zyndo seller adopt <agentId> Rebind current seller.yaml slug to an existing agentId
|
|
106
|
+
zyndo seller retire <slug> Retire a seller slug (frees a cap slot)
|
|
99
107
|
zyndo version Show version
|
|
100
108
|
zyndo help Show this help
|
|
101
109
|
|
package/dist/init.js
CHANGED
|
@@ -171,18 +171,23 @@ function testBinary(binary) {
|
|
|
171
171
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
+
// Default model is intentionally blank for every harness. Each CLI (claude,
|
|
175
|
+
// codex) has its own native default that auto-tracks whatever the provider
|
|
176
|
+
// currently rolls out on the seller's account. Hardcoding a string here used
|
|
177
|
+
// to break every seller when OpenAI / Anthropic rotated their allowlists.
|
|
178
|
+
// Sellers who want to pin a specific model can still enter one at the prompt.
|
|
174
179
|
const HARNESS_OPTIONS = [
|
|
175
|
-
{ label: 'Claude Code', harness: 'claude', binary: 'claude', defaultModel: '
|
|
176
|
-
{ label: 'Codex CLI', harness: 'codex', binary: 'codex', defaultModel: '
|
|
177
|
-
{ label: 'Other / Custom binary', harness: 'generic', binary: '', defaultModel: '
|
|
180
|
+
{ label: 'Claude Code', harness: 'claude', binary: 'claude', defaultModel: '' },
|
|
181
|
+
{ label: 'Codex CLI', harness: 'codex', binary: 'codex', defaultModel: '' },
|
|
182
|
+
{ label: 'Other / Custom binary', harness: 'generic', binary: '', defaultModel: '' }
|
|
178
183
|
];
|
|
179
184
|
function detectInstalledHarness() {
|
|
180
185
|
const claudeBin = findClaudeBinary();
|
|
181
186
|
if (claudeBin !== undefined)
|
|
182
|
-
return { harness: 'claude', binary: claudeBin, model: '
|
|
187
|
+
return { harness: 'claude', binary: claudeBin, model: '' };
|
|
183
188
|
const codexBin = findCodexBinary();
|
|
184
189
|
if (codexBin !== undefined)
|
|
185
|
-
return { harness: 'codex', binary: codexBin, model: '
|
|
190
|
+
return { harness: 'codex', binary: codexBin, model: '' };
|
|
186
191
|
return undefined;
|
|
187
192
|
}
|
|
188
193
|
function parseFlags(argv) {
|
|
@@ -506,11 +511,20 @@ const YAML_HEADER = `# Zyndo seller configuration.
|
|
|
506
511
|
# provider Always 'claude-code' (the spawn-a-CLI provider).
|
|
507
512
|
# harness 'claude' | 'codex' | 'generic'. MUST match the binary below.
|
|
508
513
|
# claude_code_binary Absolute path to your harness binary. Set by \`zyndo init\`.
|
|
509
|
-
# model Model name passed to the harness
|
|
514
|
+
# model OPTIONAL. Model name passed to the harness via -m/--model.
|
|
515
|
+
# Leave blank (or omit entirely) to use the harness CLI's
|
|
516
|
+
# own default — recommended, since it auto-tracks whatever
|
|
517
|
+
# model your Claude/Codex account currently supports.
|
|
510
518
|
# working_directory Where the seller can read/write files.
|
|
511
519
|
# max_concurrent_tasks How many tasks the daemon will run in parallel.
|
|
512
520
|
# skills What you offer. price_cents is REQUIRED ($0.10 floor).
|
|
513
521
|
# categories Free-form tags used in marketplace browse filters.
|
|
522
|
+
# force_new_slug OPTIONAL, default false. Set to true ONLY if you
|
|
523
|
+
# deliberately want a second empty seller on this
|
|
524
|
+
# account that shares a display name with an existing
|
|
525
|
+
# one. Normally leave it unset — the broker's orphan
|
|
526
|
+
# guard will tell you to adopt the existing agentId
|
|
527
|
+
# instead of silently minting a duplicate.
|
|
514
528
|
`;
|
|
515
529
|
/**
|
|
516
530
|
* If a seller.yaml already exists, return its `id:` (slug) if any. Used so
|
|
@@ -553,7 +567,9 @@ function writeConfig(out, force) {
|
|
|
553
567
|
provider: 'claude-code',
|
|
554
568
|
harness: out.harness,
|
|
555
569
|
claude_code_binary: out.binary,
|
|
556
|
-
model
|
|
570
|
+
// Omit `model` entirely when blank so the generated yaml stays clean.
|
|
571
|
+
// An absent `model:` line means the harness CLI picks its own default.
|
|
572
|
+
...(out.model.length > 0 ? { model: out.model } : {}),
|
|
557
573
|
working_directory: out.workingDirectory,
|
|
558
574
|
max_concurrent_tasks: 1,
|
|
559
575
|
identity_key_path: identityKeyPath,
|
|
@@ -683,19 +699,11 @@ function runNonInteractive(flags) {
|
|
|
683
699
|
throw new Error('No AI harness found. Install Claude Code or Codex CLI, or pass --harness and --binary explicitly.');
|
|
684
700
|
}
|
|
685
701
|
}
|
|
686
|
-
// Pick model: explicit > harness default
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
model = 'sonnet';
|
|
692
|
-
}
|
|
693
|
-
else if (harness === 'codex') {
|
|
694
|
-
model = 'gpt-5-codex';
|
|
695
|
-
}
|
|
696
|
-
else {
|
|
697
|
-
model = 'default';
|
|
698
|
-
}
|
|
702
|
+
// Pick model: explicit > blank (harness CLI uses its own default).
|
|
703
|
+
// Blank is safer than a hardcoded name because providers rotate model
|
|
704
|
+
// availability on ChatGPT/Claude account plans and any stale pin breaks
|
|
705
|
+
// every task overnight. See `HARNESS_OPTIONS` comment above.
|
|
706
|
+
model = flags.model ?? '';
|
|
699
707
|
// Functional test
|
|
700
708
|
const test = testBinary(binary);
|
|
701
709
|
if (!test.ok) {
|
|
@@ -820,6 +828,7 @@ async function runInteractive() {
|
|
|
820
828
|
process.stdout.write(' rejects any buyer brief that exceeds it. Be specific and strict.\n\n');
|
|
821
829
|
const deliverables = await promptDeliverables(ask, skillName);
|
|
822
830
|
process.stdout.write('\n');
|
|
831
|
+
process.stdout.write(' \x1b[2m(leave blank to use your harness CLI\'s own default — recommended)\x1b[0m\n');
|
|
823
832
|
const model = await ask('Model', selected.defaultModel);
|
|
824
833
|
const workingDir = await ask('Working directory', process.cwd());
|
|
825
834
|
process.stdout.write('\n \x1b[1mZyndo Marketplace\x1b[0m\n');
|
|
@@ -28,11 +28,16 @@ export function detectHarness(binary) {
|
|
|
28
28
|
function buildHarnessSpawn(harness, config, systemPrompt) {
|
|
29
29
|
switch (harness) {
|
|
30
30
|
case 'claude': {
|
|
31
|
+
// Only pass --model when the seller explicitly pinned one in seller.yaml.
|
|
32
|
+
// Empty model lets Claude Code use its own built-in default, which
|
|
33
|
+
// auto-tracks whatever Anthropic currently rolls out. Prevents stale
|
|
34
|
+
// seller.yaml pins (e.g. `sonnet-3`) from overriding a working default.
|
|
35
|
+
const modelArgs = config.model.length > 0 ? ['--model', config.model] : [];
|
|
31
36
|
const args = [
|
|
32
37
|
'--print',
|
|
33
38
|
'--output-format', 'json',
|
|
34
39
|
'--system-prompt', systemPrompt,
|
|
35
|
-
|
|
40
|
+
...modelArgs,
|
|
36
41
|
'--add-dir', config.workingDirectory,
|
|
37
42
|
'--permission-mode', 'bypassPermissions',
|
|
38
43
|
'--no-session-persistence'
|
|
@@ -53,10 +58,18 @@ function buildHarnessSpawn(harness, config, systemPrompt) {
|
|
|
53
58
|
// events that would otherwise pollute parseOutput.
|
|
54
59
|
// - `--color never` strips ANSI escapes from any stderr we might log.
|
|
55
60
|
const outputFile = join(tmpdir(), `zyndo-codex-${Date.now()}-${randomBytes(4).toString('hex')}.txt`);
|
|
61
|
+
// Only pass -m when the seller explicitly pinned a model. Empty model
|
|
62
|
+
// lets Codex use its own ~/.codex/config.toml default, which
|
|
63
|
+
// auto-tracks whatever OpenAI currently supports on the seller's plan.
|
|
64
|
+
// Incident 2026-04-14: a hardcoded `gpt-5-codex` override broke every
|
|
65
|
+
// task when OpenAI rotated the ChatGPT-edu allowlist to `gpt-5.3-codex`
|
|
66
|
+
// overnight. Blank is the safest default; sellers can still pin when
|
|
67
|
+
// they know they need a specific model.
|
|
68
|
+
const modelArgs = config.model.length > 0 ? ['-m', config.model] : [];
|
|
56
69
|
const args = [
|
|
57
70
|
'exec',
|
|
58
71
|
'-',
|
|
59
|
-
|
|
72
|
+
...modelArgs,
|
|
60
73
|
'--dangerously-bypass-approvals-and-sandbox',
|
|
61
74
|
'--skip-git-repo-check',
|
|
62
75
|
'-C', config.workingDirectory,
|
|
@@ -209,18 +222,32 @@ export async function runClaudeCodeTask(taskContext, config, logger) {
|
|
|
209
222
|
clearTimeout(timeoutHandle);
|
|
210
223
|
if (err.name === 'AbortError') {
|
|
211
224
|
logger.error(`Task timed out after ${timeoutMs / 1000}s`);
|
|
212
|
-
|
|
225
|
+
// timedOut path — caller already treats this as non-deliverable
|
|
226
|
+
// and sends an info message instead of calling deliverTask. Keep
|
|
227
|
+
// the output field empty so there's no accidental leak.
|
|
228
|
+
resolve({ output: '', paused: false, timedOut: true });
|
|
213
229
|
return;
|
|
214
230
|
}
|
|
215
231
|
logger.error(`Spawn error: ${err.message}`);
|
|
216
|
-
resolve({
|
|
232
|
+
resolve({
|
|
233
|
+
output: '',
|
|
234
|
+
paused: false,
|
|
235
|
+
harnessFailed: true,
|
|
236
|
+
harnessError: `Failed to spawn harness: ${err.message}`
|
|
237
|
+
});
|
|
217
238
|
});
|
|
218
239
|
proc.on('close', (code) => {
|
|
219
240
|
clearTimeout(timeoutHandle);
|
|
220
241
|
const stdout = Buffer.concat(stdoutChunks).toString('utf-8');
|
|
221
242
|
const stderr = Buffer.concat(stderrChunks).toString('utf-8');
|
|
222
243
|
if (stderr.length > 0) {
|
|
223
|
-
|
|
244
|
+
// Dump the full stderr tail so we can see the actual codex error.
|
|
245
|
+
// Codex prints its startup banner first, then the real error below,
|
|
246
|
+
// so a 500-char slice would hide everything useful. Cap at 8000
|
|
247
|
+
// to avoid pathological logs.
|
|
248
|
+
const STDERR_MAX = 8000;
|
|
249
|
+
const tail = stderr.length > STDERR_MAX ? stderr.slice(-STDERR_MAX) : stderr;
|
|
250
|
+
logger.info(`Harness stderr (len=${stderr.length}):\n${tail}`);
|
|
224
251
|
}
|
|
225
252
|
// Codex writes the agent's final message to a tempfile via `-o`.
|
|
226
253
|
// Read it back, then unlink. If the file is missing or unreadable
|
|
@@ -239,8 +266,22 @@ export async function runClaudeCodeTask(taskContext, config, logger) {
|
|
|
239
266
|
catch { /* ignore */ }
|
|
240
267
|
}
|
|
241
268
|
if (code !== 0 && agentOutput.length === 0) {
|
|
242
|
-
|
|
243
|
-
|
|
269
|
+
// Log the full stderr tail (already emitted above at INFO, but make
|
|
270
|
+
// sure the ERROR line also carries enough context to diagnose).
|
|
271
|
+
const stderrTail = stderr.length > 4000 ? stderr.slice(-4000) : stderr;
|
|
272
|
+
logger.error(`Harness exited with code ${code}. stderr tail:\n${stderrTail}`);
|
|
273
|
+
// CRITICAL: do NOT put the stderr dump into `output` — the caller
|
|
274
|
+
// would deliver that verbatim to the buyer as the task deliverable.
|
|
275
|
+
// Signal failure via the harnessFailed flag so handleTask can send
|
|
276
|
+
// a human-readable info message to the buyer instead. Incident
|
|
277
|
+
// 2026-04-14: a failing codex run was delivered as "Harness failed
|
|
278
|
+
// (exit 1): OpenAI Codex ..." which the buyer saw as their post.
|
|
279
|
+
resolve({
|
|
280
|
+
output: '',
|
|
281
|
+
paused: false,
|
|
282
|
+
harnessFailed: true,
|
|
283
|
+
harnessError: `Codex exited ${code}. Try requesting a revision to retry.`
|
|
284
|
+
});
|
|
244
285
|
return;
|
|
245
286
|
}
|
|
246
287
|
resolve(parseOutput(agentOutput, harness));
|
package/dist/sellerDaemon.js
CHANGED
|
@@ -10,6 +10,7 @@ import { createAnthropicProvider } from './providers/anthropic.js';
|
|
|
10
10
|
import { createOpenAIProvider } from './providers/openai.js';
|
|
11
11
|
import { createOllamaProvider } from './providers/ollama.js';
|
|
12
12
|
import { runClaudeCodeTask } from './providers/claudeCode.js';
|
|
13
|
+
import { verifyHarnessReady } from './harnessSelfCheck.js';
|
|
13
14
|
import { createReadFileTool } from './tools/readFile.js';
|
|
14
15
|
import { createWriteFileTool } from './tools/writeFile.js';
|
|
15
16
|
import { createBashTool } from './tools/bash.js';
|
|
@@ -120,6 +121,21 @@ export async function startSellerDaemon(config, opts) {
|
|
|
120
121
|
catch (err) {
|
|
121
122
|
logger.error(`Identity key setup failed: ${err instanceof Error ? err.message : String(err)}. Continuing unsigned.`);
|
|
122
123
|
}
|
|
124
|
+
// Harness self-check: before touching the broker, spawn the configured
|
|
125
|
+
// harness with a trivial prompt to verify it can actually produce output.
|
|
126
|
+
// Catches stale model pins, expired auth, missing binaries, and rate
|
|
127
|
+
// limits — every failure mode that would otherwise burn a real buyer
|
|
128
|
+
// hire. Fail fast with an actionable fix instead of accepting work we
|
|
129
|
+
// know we cannot deliver. Incident 2026-04-14.
|
|
130
|
+
logger.info('Running harness self-check…');
|
|
131
|
+
const selfCheck = await verifyHarnessReady(config);
|
|
132
|
+
if (!selfCheck.ok) {
|
|
133
|
+
logger.error(`Harness self-check FAILED: ${selfCheck.reason}`);
|
|
134
|
+
logger.error(`Fix:\n ${selfCheck.fix}`);
|
|
135
|
+
logger.error('Daemon refusing to start. Fix the issue above and re-run "zyndo serve".');
|
|
136
|
+
throw new Error(`Harness self-check failed: ${selfCheck.reason}`);
|
|
137
|
+
}
|
|
138
|
+
logger.info('Harness self-check passed.');
|
|
123
139
|
// Try to reconnect as the same agent from a previous session
|
|
124
140
|
let session;
|
|
125
141
|
const previousSession = loadSession();
|
|
@@ -153,7 +169,8 @@ export async function startSellerDaemon(config, opts) {
|
|
|
153
169
|
skills: [...config.skills],
|
|
154
170
|
categories: [...config.categories],
|
|
155
171
|
maxConcurrentTasks: config.maxConcurrentTasks,
|
|
156
|
-
sellerSlug: config.id
|
|
172
|
+
sellerSlug: config.id,
|
|
173
|
+
forceNewSlug: config.forceNewSlug
|
|
157
174
|
});
|
|
158
175
|
logger.info(`Connected: agentId=${session.agentId}`);
|
|
159
176
|
}
|
|
@@ -603,6 +620,19 @@ async function handleTask(holder, taskId, config, logger, identityPrivateKey) {
|
|
|
603
620
|
saveState({ taskId, messages: [], claudeCodeContext: context, originalContext: context });
|
|
604
621
|
return;
|
|
605
622
|
}
|
|
623
|
+
// Harness failure (codex/claude/generic exited non-zero with no output).
|
|
624
|
+
// NEVER deliver in this branch — the caller's `output` would be empty or
|
|
625
|
+
// a stderr dump and the buyer would receive it as their deliverable.
|
|
626
|
+
// Send a human-readable info message instead and leave the task in its
|
|
627
|
+
// current state so the buyer can request a revision to retry or open a
|
|
628
|
+
// dispute. Incident 2026-04-14.
|
|
629
|
+
if (result.harnessFailed === true) {
|
|
630
|
+
const reason = result.harnessError ?? 'The seller agent crashed during this run.';
|
|
631
|
+
logger.error(`Task ${taskId}: harness failed — ${reason}. NOT delivering stderr.`);
|
|
632
|
+
await sendTaskMessage(holder, taskId, 'info', `The seller agent encountered an error and could not produce a deliverable on this attempt. Details: ${reason}. Please click "Request revision" with the same brief to retry, or open a dispute if the problem persists.`);
|
|
633
|
+
saveState({ taskId, messages: [], claudeCodeContext: context, originalContext: context });
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
606
636
|
if (result.paused) {
|
|
607
637
|
logger.info(`Task ${taskId}: paused, asking buyer: "${result.pendingQuestion?.slice(0, 100)}..."`);
|
|
608
638
|
saveState({ taskId, messages: [], claudeCodeContext: context, pendingQuestion: result.pendingQuestion });
|
|
@@ -698,6 +728,13 @@ async function handleRevision(holder, taskId, feedback, config, logger, identity
|
|
|
698
728
|
saveState({ taskId, messages: [], claudeCodeContext: revisionContext, originalContext });
|
|
699
729
|
return;
|
|
700
730
|
}
|
|
731
|
+
if (result.harnessFailed === true) {
|
|
732
|
+
const reason = result.harnessError ?? 'The seller agent crashed during the revision.';
|
|
733
|
+
logger.error(`Task ${taskId}: harness failed on revision — ${reason}. NOT delivering stderr.`);
|
|
734
|
+
await sendTaskMessage(holder, taskId, 'info', `The seller agent hit an error while reworking this delivery (${reason}). Try requesting another revision, or open a dispute if the issue persists.`);
|
|
735
|
+
saveState({ taskId, messages: [], claudeCodeContext: revisionContext, originalContext });
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
701
738
|
if (result.paused) {
|
|
702
739
|
logger.info(`Task ${taskId}: revision paused, asking buyer...`);
|
|
703
740
|
saveState({ taskId, messages: [], claudeCodeContext: revisionContext, originalContext, pendingQuestion: result.pendingQuestion });
|