mirror of
https://github.com/openclaw/lobster.git
synced 2026-08-14 00:48:09 +00:00
* fix(runtime): stop process-backed work on cancellation
* fix(runtime): invalidate cancelled resume state
* Revert "fix(runtime): invalidate cancelled resume state"
This reverts commit 0f7d291f98.
* fix(runtime): narrow cancellation to safe child processes
* fix(runtime): stop after completed search cancellation
* fix(runtime): halt direct pipelines after cancellation
Preserve completed in-flight stage results while preventing later direct pipeline stages from starting after parent cancellation.
* fix(runtime): consume aborted approval resumes
* fix(runtime): cancelled workflows no longer continue external commands
* fix(workflow): propagate custom parent cancellation
* fix(runtime): preserve pre-aborted resume state
* fix(runtime): stop lazy handoff after cancellation
* fix(workflow): close remaining cancellation boundaries
* fix(runtime): preserve workflow resumes during setup cancellation
* fix(runtime): close final cancellation persistence gaps
* fix(runtime): stop lazy handoff after cancellation
* fix(runtime): interrupt blocked lazy handoff reads
* fix(runtime): terminate cancellation process trees
* fix(runtime): await process tree termination
* fix(runtime): terminate workflow process trees
* fix(runtime): bridge CLI cancellation
* fix(cli): preserve cancellation lifecycle
* fix(cli): abort stalled signal-aware commands
* fix(cli): release aborted interactive prompts
* fix(cli): preserve sequential prompt input
* fix(cli): preserve buffered prompt input
* fix(cli): handle prompt EOF after buffered input
* fix(runtime): preserve UTF-8 subprocess output
* fix(workflow): preserve retryable resume before execution
* fix(workflow): roll back cancelled resume replacement
* fix(state): roll back cancelled monitor snapshot
* fix(resume): preserve cancelled gate capabilities
* fix(resume): close cancellation rollback windows
* fix(resume): harden cancellation state cleanup
* fix(runtime): close resumed cancellation gaps
* fix(runtime): preserve cancellation cleanup
* fix(runtime): close cancellation lifecycle gaps
* fix(runtime): harden resumed cancellation boundaries
* fix(workflow): consume timed-out resume capabilities
* fix(workflow): preserve resume policy boundaries
* fix(runtime): preserve cancellation cleanup liveness
* fix(runtime): harden cancellation cleanup
* fix(runtime): stop lazy output after cancellation
* fix(runtime): settle cancellation cleanup
* fix(runtime): preserve safe input resumes
* fix(runtime): prevent consumed resume replays
* fix(runtime): serialize approval resume consumption
* fix(runtime): prevent concurrent safe gate forks
* fix(runtime): close cancellation review gaps
* fix(llm): restore cache after cancelled refresh
* fix(runtime): close remaining cancellation windows
* fix(runtime): preserve cancellation recovery invariants
* fix(runtime): prevent stale resume recovery
* fix(runtime): preserve resume claim recovery
* fix(runtime): retry pre-dispatch claims safely
* fix(state): synchronize rollback-safe reads
* fix(resume): discard cancelled pipeline successors
* fix(runtime): harden cancellation and state locking
* fix(runtime): recover durable cancellation failures
* fix(runtime): preserve legacy workflow cancellation
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
183 lines
4.1 KiB
TypeScript
183 lines
4.1 KiB
TypeScript
import { runAbortableProcess } from "../abortable_process.js";
|
|
|
|
async function runProcess(command, argv, { env, cwd, signal, forceTerminationSignal }) {
|
|
const { stdout, stderr, code } = await runAbortableProcess({
|
|
command,
|
|
argv,
|
|
env,
|
|
cwd,
|
|
signal,
|
|
forceTerminationSignal,
|
|
notFoundMessage: "gh not found on PATH (install GitHub CLI)",
|
|
});
|
|
if (code === 0) return { stdout, stderr };
|
|
throw new Error(`gh failed (${code}): ${stderr.trim() || stdout.trim()}`);
|
|
}
|
|
|
|
import { diffAndStore } from "../state/store.js";
|
|
|
|
function pickSubset(snapshot) {
|
|
if (!snapshot || typeof snapshot !== "object") return null;
|
|
return {
|
|
number: snapshot.number,
|
|
title: snapshot.title,
|
|
url: snapshot.url,
|
|
state: snapshot.state,
|
|
isDraft: snapshot.isDraft,
|
|
mergeable: snapshot.mergeable,
|
|
reviewDecision: snapshot.reviewDecision,
|
|
updatedAt: snapshot.updatedAt,
|
|
baseRefName: snapshot.baseRefName,
|
|
headRefName: snapshot.headRefName,
|
|
};
|
|
}
|
|
|
|
export function buildPrChangeSummary(before, after) {
|
|
const a = pickSubset(after);
|
|
const b = pickSubset(before);
|
|
|
|
if (!a) return { changedFields: [], changes: {} };
|
|
if (!b) {
|
|
return {
|
|
changedFields: Object.keys(a),
|
|
changes: Object.fromEntries(Object.keys(a).map((k) => [k, { from: null, to: a[k] }])),
|
|
};
|
|
}
|
|
|
|
const changes = {};
|
|
for (const key of Object.keys(a)) {
|
|
if (JSON.stringify(a[key]) !== JSON.stringify(b[key])) {
|
|
changes[key] = { from: b[key], to: a[key] };
|
|
}
|
|
}
|
|
|
|
return {
|
|
changedFields: Object.keys(changes),
|
|
changes,
|
|
};
|
|
}
|
|
|
|
function formatPrChangeMessage({ repo, pr, changedFields, prInfo }) {
|
|
const fields = changedFields.length ? ` (${changedFields.join(", ")})` : "";
|
|
const title = prInfo?.title ? `: ${prInfo.title}` : "";
|
|
const url = prInfo?.url ? ` ${prInfo.url}` : "";
|
|
return `PR updated: ${repo}#${pr}${title}${fields}.${url}`.replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
export async function runGithubPrMonitorWorkflow({ args, ctx }) {
|
|
ctx.signal?.throwIfAborted();
|
|
const repo = args.repo;
|
|
const pr = args.pr;
|
|
if (!repo || !pr) throw new Error("github.pr.monitor requires args.repo and args.pr");
|
|
|
|
const key = args.key ?? `github.pr:${repo}#${pr}`;
|
|
const changesOnly = Boolean(args.changesOnly);
|
|
const summaryOnly = Boolean(args.summaryOnly);
|
|
|
|
const argv = [
|
|
"pr",
|
|
"view",
|
|
String(pr),
|
|
"--repo",
|
|
String(repo),
|
|
"--json",
|
|
"number,title,url,state,isDraft,mergeable,reviewDecision,author,baseRefName,headRefName,updatedAt",
|
|
];
|
|
|
|
const { stdout } = (await runProcess("gh", argv, {
|
|
env: ctx.env,
|
|
cwd: process.cwd(),
|
|
signal: ctx.signal,
|
|
forceTerminationSignal: ctx.forceTerminationSignal,
|
|
})) as any;
|
|
ctx.signal?.throwIfAborted();
|
|
|
|
let current;
|
|
try {
|
|
current = JSON.parse(stdout.trim());
|
|
} catch {
|
|
throw new Error("gh returned non-JSON output");
|
|
}
|
|
|
|
const { changed, before } = await diffAndStore({
|
|
env: ctx.env,
|
|
key,
|
|
value: current,
|
|
signal: ctx.signal,
|
|
});
|
|
|
|
if (changesOnly && !changed) {
|
|
return {
|
|
kind: "github.pr.monitor",
|
|
repo,
|
|
prNumber: Number(pr),
|
|
key,
|
|
changed: false,
|
|
suppressed: true,
|
|
};
|
|
}
|
|
|
|
const summary = buildPrChangeSummary(before, current);
|
|
|
|
if (summaryOnly) {
|
|
return {
|
|
kind: "github.pr.monitor",
|
|
repo,
|
|
prNumber: Number(pr),
|
|
key,
|
|
changed,
|
|
summary,
|
|
pr: {
|
|
number: current.number,
|
|
title: current.title,
|
|
url: current.url,
|
|
state: current.state,
|
|
updatedAt: current.updatedAt,
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
kind: "github.pr.monitor",
|
|
repo,
|
|
prNumber: Number(pr),
|
|
key,
|
|
changed,
|
|
summary,
|
|
prSnapshot: current,
|
|
};
|
|
}
|
|
|
|
export async function runGithubPrMonitorNotifyWorkflow({ args, ctx }) {
|
|
const base = await runGithubPrMonitorWorkflow({
|
|
args: {
|
|
...args,
|
|
changesOnly: true,
|
|
summaryOnly: true,
|
|
},
|
|
ctx,
|
|
});
|
|
|
|
if (base.suppressed) {
|
|
return { kind: "github.pr.monitor.notify", suppressed: true };
|
|
}
|
|
|
|
const changedFields = base.summary?.changedFields ?? [];
|
|
const prInfo = base.pr ?? {};
|
|
|
|
return {
|
|
kind: "github.pr.monitor.notify",
|
|
changed: Boolean(base.changed),
|
|
repo: args.repo,
|
|
prNumber: Number(args.pr),
|
|
message: formatPrChangeMessage({
|
|
repo: args.repo,
|
|
pr: Number(args.pr),
|
|
changedFields,
|
|
prInfo,
|
|
}),
|
|
pr: prInfo,
|
|
summary: base.summary,
|
|
};
|
|
}
|