Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 268 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 35 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { parsePipeline } from './parser.js';
import { createDefaultRegistry } from './commands/registry.js';
import { runPipeline } from './runtime.js';
import { encodeToken } from './token.js';
import { decodeResumeToken, parseResumeArgs } from './resume.js';
import { decodeResumeToken, parseResumeArgs, resolveApprovalId } from './resume.js';
import { cleanupApprovalIndexByStateKey, deleteApprovalId, generateApprovalId, writeApprovalIndex } from './state/store.js';
import { runWorkflowFile } from './workflows/file.js';
import { randomUUID } from 'node:crypto';
import { deleteStateJson, readStateJson, writeStateJson } from './state/store.js';
Expand Down Expand Up @@ -172,13 +173,15 @@ async function handleRun({ argv, registry }) {
: null;

if (approval) {
const aid = generateApprovalId();
const stateKey = await savePipelineResumeState(process.env, {
pipeline,
resumeAtIndex: (output.haltedAt?.index ?? -1) + 1,
items: approval.items,
prompt: approval.prompt,
createdAt: new Date().toISOString(),
});
await writeApprovalIndex({ env: process.env, stateKey, approvalId: aid });

const resumeToken = encodeToken({
protocolVersion: 1,
Expand All @@ -194,6 +197,7 @@ async function handleRun({ argv, registry }) {
requiresApproval: {
...approval,
resumeToken,
approvalId: aid,
},
});
return;
Expand Down Expand Up @@ -315,17 +319,37 @@ async function handleResume({ argv, registry }) {
const mode = 'tool';
let approved: boolean;
let payload: any;
let resolvedApprovalId: string | null = null;
try {
const parsed = parseResumeArgs(argv);
approved = parsed.approved;
payload = decodeResumeToken(parsed.token);
resolvedApprovalId = parsed.approvalId;

// Resolve short approval ID to token if provided
let token: string;
if (parsed.approvalId) {
token = await resolveApprovalId(parsed.approvalId, process.env);
} else {
token = parsed.token!;
}
payload = decodeResumeToken(token);
} catch (err) {
writeToolEnvelope({ ok: false, error: { type: 'parse_error', message: err?.message ?? String(err) } });
process.exitCode = 2;
return;
}

// Helper: clean up approval ID index after successful use
const cleanupIndex = async () => {
if (resolvedApprovalId) {
await deleteApprovalId({ env: process.env, approvalId: resolvedApprovalId });
} else if (payload.stateKey) {
await cleanupApprovalIndexByStateKey({ env: process.env, stateKey: payload.stateKey });
}
};

if (!approved) {
await cleanupIndex();
if (payload.kind === 'workflow-file' && payload.stateKey) {
await deleteStateJson({ env: process.env, key: payload.stateKey });
}
Expand Down Expand Up @@ -362,9 +386,11 @@ async function handleResume({ argv, registry }) {
return;
}

await cleanupIndex();
writeToolEnvelope({ ok: true, status: 'ok', output: output.output, requiresApproval: null });
return;
} catch (err) {
// Don't clean up index on error — allow retry by --id
writeToolEnvelope({ ok: false, error: { type: 'runtime_error', message: err?.message ?? String(err) } });
process.exitCode = 1;
return;
Expand Down Expand Up @@ -406,8 +432,12 @@ async function handleResume({ argv, registry }) {
prompt: approval.prompt,
createdAt: new Date().toISOString(),
});
await cleanupIndex();
await deleteStateJson({ env: process.env, key: previousStateKey });
Comment on lines +435 to 436
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Write next approval index before deleting prior resume state

In the pipeline multi-approval path, the old alias/state are deleted before writing the new approval index. If writeApprovalIndex then fails (for example due to I/O errors like ENOSPC/EACCES), the handler returns runtime_error after removing the prior resumable state and without returning a usable new alias, leaving the user unable to continue from either --id or the old token.

Useful? React with 👍 / 👎.


const nextAid = generateApprovalId();
await writeApprovalIndex({ env: process.env, stateKey: nextStateKey, approvalId: nextAid });

const resumeToken = encodeToken({
protocolVersion: 1,
v: 1,
Expand All @@ -419,14 +449,16 @@ async function handleResume({ argv, registry }) {
ok: true,
status: 'needs_approval',
output: [],
requiresApproval: { ...approval, resumeToken },
requiresApproval: { ...approval, resumeToken, approvalId: nextAid },
});
return;
}

await cleanupIndex();
await deleteStateJson({ env: process.env, key: previousStateKey });
writeToolEnvelope({ ok: true, status: 'ok', output: output.items, requiresApproval: null });
} catch (err) {
// Don't clean up index on error — allow retry by --id
writeToolEnvelope({ ok: false, error: { type: 'runtime_error', message: err?.message ?? String(err) } });
process.exitCode = 1;
}
Expand Down
Loading