@atolis-hq/wake 0.2.59 → 0.2.60
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseClaudePrintResult, parseRunnerResult } from '../../domain/schema.js';
|
|
2
2
|
import { runAgentCliCommand } from '../runner/cli-command.js';
|
|
3
|
-
import { buildStagePrompt } from '../runner/stage-prompt.js';
|
|
3
|
+
import { buildStagePrompt, sentinelListForApproval } from '../runner/stage-prompt.js';
|
|
4
4
|
import { emitRuntimeEvent, runnerRuntimeEvent } from '../runner/runtime-events.js';
|
|
5
5
|
import { writeRunnerTranscript } from '../runner/transcripts.js';
|
|
6
6
|
import { createAgentExecution } from '../../core/live-execution.js';
|
|
@@ -120,6 +120,80 @@ function extractTokenUsage(parsed) {
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
const CLAUDE_CLI_NAME = 'Claude';
|
|
123
|
+
// Bounded — this is a single "just tell me the status" follow-up, not a
|
|
124
|
+
// second attempt at the task, so it never needs more than one turn.
|
|
125
|
+
const ENVELOPE_REPAIR_MAX_TURNS = 1;
|
|
126
|
+
const ENVELOPE_REPAIR_TIMEOUT_MS = 60_000;
|
|
127
|
+
function buildEnvelopeRepairPrompt(skipApproval) {
|
|
128
|
+
return [
|
|
129
|
+
'Your previous reply did not end with the required `wake-result` envelope, so it could not be parsed.',
|
|
130
|
+
'Reply with ONLY a fenced `wake-result` JSON block containing a `status` field, then repeat that status word on its own line after the closing fence.',
|
|
131
|
+
`The status must be exactly one of: ${sentinelListForApproval(skipApproval)}, reflecting the outcome of your previous turn.`,
|
|
132
|
+
'Do not repeat, summarize, or redo any of your previous work — this reply is parsed automatically and anything besides the envelope is discarded.',
|
|
133
|
+
].join('\n');
|
|
134
|
+
}
|
|
135
|
+
function mergeTokenUsage(base, extra) {
|
|
136
|
+
if (extra === undefined) {
|
|
137
|
+
return base;
|
|
138
|
+
}
|
|
139
|
+
if (base === undefined) {
|
|
140
|
+
return extra;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
inputTokens: base.inputTokens + extra.inputTokens,
|
|
144
|
+
outputTokens: base.outputTokens + extra.outputTokens,
|
|
145
|
+
...(base.cacheCreationInputTokens === undefined && extra.cacheCreationInputTokens === undefined
|
|
146
|
+
? {}
|
|
147
|
+
: {
|
|
148
|
+
cacheCreationInputTokens: (base.cacheCreationInputTokens ?? 0) + (extra.cacheCreationInputTokens ?? 0),
|
|
149
|
+
}),
|
|
150
|
+
...(base.cacheReadInputTokens === undefined && extra.cacheReadInputTokens === undefined
|
|
151
|
+
? {}
|
|
152
|
+
: {
|
|
153
|
+
cacheReadInputTokens: (base.cacheReadInputTokens ?? 0) + (extra.cacheReadInputTokens ?? 0),
|
|
154
|
+
}),
|
|
155
|
+
...(base.costUsd === undefined && extra.costUsd === undefined
|
|
156
|
+
? {}
|
|
157
|
+
: { costUsd: (base.costUsd ?? 0) + (extra.costUsd ?? 0) }),
|
|
158
|
+
...(base.turns === undefined && extra.turns === undefined
|
|
159
|
+
? {}
|
|
160
|
+
: { turns: (base.turns ?? 0) + (extra.turns ?? 0) }),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// Asks the same (still-live) session to restate just its result envelope,
|
|
164
|
+
// for the case where a run otherwise completed but the model forgot the
|
|
165
|
+
// mandatory trailer — cheaper and more honest than defaulting the whole run
|
|
166
|
+
// to BLOCKED because of a formatting slip. Returns undefined on any failure
|
|
167
|
+
// so the caller falls back to the original (unparseable) result untouched.
|
|
168
|
+
async function attemptEnvelopeRepair(input) {
|
|
169
|
+
const args = buildClaudePrintArgs({
|
|
170
|
+
model: input.model,
|
|
171
|
+
prompt: buildEnvelopeRepairPrompt(input.skipApproval),
|
|
172
|
+
sessionName: input.sessionName,
|
|
173
|
+
resumeSessionId: input.sessionId,
|
|
174
|
+
maxTurns: ENVELOPE_REPAIR_MAX_TURNS,
|
|
175
|
+
});
|
|
176
|
+
const result = await runClaudeCommand({
|
|
177
|
+
command: input.command,
|
|
178
|
+
args,
|
|
179
|
+
cwd: input.cwd,
|
|
180
|
+
timeoutMs: input.timeoutMs,
|
|
181
|
+
});
|
|
182
|
+
if (result.exitCode !== 0 || result.timedOut || result.stdout.trim().length === 0) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const parsed = parseClaudePrintOutput(result.stdout);
|
|
187
|
+
const repairTokenUsage = extractTokenUsage(parsed);
|
|
188
|
+
return {
|
|
189
|
+
text: parsed.result,
|
|
190
|
+
...(repairTokenUsage === undefined ? {} : { tokenUsage: repairTokenUsage }),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
123
197
|
export function classifyClaudeCliFailure(input) {
|
|
124
198
|
if (input.timedOut) {
|
|
125
199
|
return 'infra';
|
|
@@ -341,6 +415,30 @@ export function createClaudeRunner(options) {
|
|
|
341
415
|
...(parsed.session_id === undefined ? {} : { sessionId: parsed.session_id }),
|
|
342
416
|
}));
|
|
343
417
|
const tokenUsage = extractTokenUsage(parsed);
|
|
418
|
+
let effectiveResultText = parsed.result;
|
|
419
|
+
let effectiveTokenUsage = tokenUsage;
|
|
420
|
+
let envelopeRepaired = false;
|
|
421
|
+
if (parseRunnerResult(effectiveResultText).envelope === 'missing' &&
|
|
422
|
+
parsed.session_id !== undefined) {
|
|
423
|
+
const repair = await attemptEnvelopeRepair({
|
|
424
|
+
command: options.command,
|
|
425
|
+
cwd: input.workspacePath ?? options.cwd,
|
|
426
|
+
model,
|
|
427
|
+
sessionName,
|
|
428
|
+
sessionId: parsed.session_id,
|
|
429
|
+
skipApproval: stagePrompt.skipApproval,
|
|
430
|
+
timeoutMs: Math.min(options.settings.timeoutMs, ENVELOPE_REPAIR_TIMEOUT_MS),
|
|
431
|
+
});
|
|
432
|
+
if (repair !== undefined && parseRunnerResult(repair.text).envelope !== 'missing') {
|
|
433
|
+
effectiveResultText = `${effectiveResultText.trimEnd()}\n\n${repair.text.trim()}`;
|
|
434
|
+
effectiveTokenUsage = mergeTokenUsage(effectiveTokenUsage, repair.tokenUsage);
|
|
435
|
+
envelopeRepaired = true;
|
|
436
|
+
console.log(`[claude-run] envelope repair succeeded runId=${input.runId} sessionId=${parsed.session_id}`);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
console.error(`[claude-run] envelope repair failed runId=${input.runId} sessionId=${parsed.session_id}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
344
442
|
if (tokenUsage !== undefined) {
|
|
345
443
|
await emitRuntimeEvent(input.onRuntimeEvent, runnerRuntimeEvent({
|
|
346
444
|
type: 'agent.usage.updated',
|
|
@@ -364,20 +462,21 @@ export function createClaudeRunner(options) {
|
|
|
364
462
|
payload: { exitCode: result.exitCode, timedOut: result.timedOut },
|
|
365
463
|
}));
|
|
366
464
|
return {
|
|
367
|
-
result:
|
|
465
|
+
result: effectiveResultText,
|
|
368
466
|
model,
|
|
369
467
|
cli: CLAUDE_CLI_NAME,
|
|
370
|
-
...(parseRunnerResult(
|
|
468
|
+
...(parseRunnerResult(effectiveResultText).status === 'FAILED'
|
|
371
469
|
? { failureClass: 'task' }
|
|
372
470
|
: {}),
|
|
373
471
|
...(parsed.session_id === undefined ? {} : { session_id: parsed.session_id }),
|
|
374
|
-
...(
|
|
472
|
+
...(effectiveTokenUsage === undefined ? {} : { tokenUsage: effectiveTokenUsage }),
|
|
375
473
|
metadata: {
|
|
376
474
|
stdout: result.stdout,
|
|
377
475
|
stderr: result.stderr,
|
|
378
476
|
raw: parsed,
|
|
379
477
|
skipApproval: stagePrompt.skipApproval,
|
|
380
478
|
allowAutoApproval: stagePrompt.allowAutoApproval,
|
|
479
|
+
...(envelopeRepaired ? { envelopeRepaired: true } : {}),
|
|
381
480
|
...(promptTranscriptPath === undefined ? {} : { promptTranscriptPath }),
|
|
382
481
|
...(responseTranscriptPath === undefined ? {} : { responseTranscriptPath }),
|
|
383
482
|
...(sandboxLog?.metadata ?? {}),
|
|
@@ -82,7 +82,7 @@ function parseFrontmatterMaxTurns(input) {
|
|
|
82
82
|
}
|
|
83
83
|
return parsed;
|
|
84
84
|
}
|
|
85
|
-
function sentinelListForApproval(skipApproval) {
|
|
85
|
+
export function sentinelListForApproval(skipApproval) {
|
|
86
86
|
return skipApproval ? 'DONE, BLOCKED, FAILED' : 'AWAITING_APPROVAL, BLOCKED, FAILED';
|
|
87
87
|
}
|
|
88
88
|
function sentinelInstructionsForApproval(skipApproval) {
|
|
@@ -121,7 +121,7 @@ function buildHarnessPrompt(input) {
|
|
|
121
121
|
if (input.upstreamChanges !== undefined && input.upstreamChanges.trim().length > 0) {
|
|
122
122
|
lines.push('', 'Upstream update notice:', 'Before resuming this session, Wake pulled the latest default-branch changes into your workspace. New commits included:', input.upstreamChanges.trimEnd());
|
|
123
123
|
}
|
|
124
|
-
lines.push('', 'Result envelope ABI:', 'Respond concisely. End your response with a fenced `wake-result` JSON block, then on its own line after the closing fence repeat the status word for degraded-mode fallback.', `The JSON \`status\` and final line must be exactly one of: ${sentinelListForApproval(input.skipApproval)}.`, sentinelInstructionsForApproval(input.skipApproval), 'The JSON object must contain only the `status` field. Do not add other fields.');
|
|
124
|
+
lines.push('', 'Result envelope ABI:', 'Respond concisely. End your response with a fenced `wake-result` JSON block, then on its own line after the closing fence repeat the status word for degraded-mode fallback.', `The JSON \`status\` and final line must be exactly one of: ${sentinelListForApproval(input.skipApproval)}.`, sentinelInstructionsForApproval(input.skipApproval), 'The JSON object must contain only the `status` field. Do not add other fields.', 'This envelope is mandatory, not optional formatting: an automated parser reads only your final lines, and a reply that omits it is discarded and treated as BLOCKED regardless of the work you actually completed.');
|
|
125
125
|
if (input.prTrackingEnabled) {
|
|
126
126
|
lines.push('', 'Artifact reporting:', 'If you created a pull request during this stage, report it before the result envelope by adding a fenced `wake-artifacts` JSON block:', '```wake-artifacts', '{ "artifacts": [{ "kind": "pr", "url": "<the PR URL>" }] }', '```', 'Only report a PR you actually created in this run. Omit the block entirely if you created no PR.');
|
|
127
127
|
}
|
|
@@ -232,8 +232,9 @@ export async function buildStagePrompt(input) {
|
|
|
232
232
|
commentSections,
|
|
233
233
|
includeRepoDetails: resolvedWorkspaceMode === 'read-only',
|
|
234
234
|
});
|
|
235
|
+
const envelopeReminder = 'Before you finish: end this reply with the `wake-result` envelope exactly as your instructions describe, or this run is discarded and marked BLOCKED.';
|
|
235
236
|
return {
|
|
236
|
-
prompt: `${renderedTemplate}\n\n${untrustedDataBlock}`,
|
|
237
|
+
prompt: `${renderedTemplate}\n\n${untrustedDataBlock}\n\n${envelopeReminder}`,
|
|
237
238
|
harnessPrompt: buildHarnessPrompt({
|
|
238
239
|
skipApproval,
|
|
239
240
|
prTrackingEnabled: input.config?.sources.github.enabled === true &&
|
|
@@ -108,7 +108,7 @@ function classifyFailedRun(input) {
|
|
|
108
108
|
? 'unknown'
|
|
109
109
|
: 'none';
|
|
110
110
|
const failurePhase = input.failurePhase ??
|
|
111
|
-
(input.envelope === 'degraded' && input.sentinel === 'FAILED'
|
|
111
|
+
((input.envelope === 'degraded' || input.envelope === 'missing') && input.sentinel === 'FAILED'
|
|
112
112
|
? 'result-parsing'
|
|
113
113
|
: failurePhaseForRecord(input.record));
|
|
114
114
|
let retrySafety;
|
|
@@ -1117,7 +1117,11 @@ export function parseRunnerResult(result) {
|
|
|
1117
1117
|
return {
|
|
1118
1118
|
status: body.length === 0 ? 'FAILED' : 'BLOCKED',
|
|
1119
1119
|
body,
|
|
1120
|
-
envelope
|
|
1120
|
+
// No structured envelope AND no recognizable bare sentinel at all —
|
|
1121
|
+
// distinct from 'degraded' (a deliberate bare-sentinel reply) so
|
|
1122
|
+
// callers can retry a runner that simply forgot the trailer instead
|
|
1123
|
+
// of trusting a fabricated BLOCKED/FAILED default.
|
|
1124
|
+
envelope: 'missing',
|
|
1121
1125
|
};
|
|
1122
1126
|
}
|
|
1123
1127
|
let removed = false;
|
package/dist/src/version.js
CHANGED