@bivy/bivy 0.10.1-staging.239 → 0.10.1-staging.241
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/github-tasks.js +32 -1
- package/dist/server.js +24 -4
- package/package.json +1 -1
package/dist/github-tasks.js
CHANGED
|
@@ -288,6 +288,35 @@ export async function ensureTaskLabels(cfg) {
|
|
|
288
288
|
export async function commentIssue(cfg, issueNumber, body) {
|
|
289
289
|
await gh(cfg, "POST", `/issues/${issueNumber}/comments`, { body });
|
|
290
290
|
}
|
|
291
|
+
/** A hidden HTML-comment marker (invisible in rendered Markdown) that makes a
|
|
292
|
+
* Bivy issue comment idempotent across process restarts and lease reclaims: the
|
|
293
|
+
* same logical comment carries the same marker, so a duplicate delivery or a
|
|
294
|
+
* reclaim on a fresh node can detect that it was already posted. */
|
|
295
|
+
export function bivyCommentMarker(key) {
|
|
296
|
+
return `<!-- bivy:comment:${key} -->`;
|
|
297
|
+
}
|
|
298
|
+
/** Whether the issue already carries a Bivy comment with the given key. Scans a
|
|
299
|
+
* single bounded page (newest issues have few comments); an API failure returns
|
|
300
|
+
* false so we prefer re-posting the signal over silently dropping it. */
|
|
301
|
+
export async function issueHasCommentMarker(cfg, issueNumber, key) {
|
|
302
|
+
const marker = bivyCommentMarker(key);
|
|
303
|
+
const res = await gh(cfg, "GET", `/issues/${issueNumber}/comments?per_page=100`);
|
|
304
|
+
if (!res.ok)
|
|
305
|
+
return false;
|
|
306
|
+
const raw = (await res.json().catch(() => []));
|
|
307
|
+
return Array.isArray(raw) && raw.some((c) => typeof c?.body === "string" && c.body.includes(marker));
|
|
308
|
+
}
|
|
309
|
+
/** Post an issue comment at most once per `(issue, key)`. Returns true when it
|
|
310
|
+
* posted, false when an identically-keyed comment already existed. This is the
|
|
311
|
+
* external-effect idempotency guard: a retry or reclaim of the same Run does not
|
|
312
|
+
* duplicate a pickup or outcome comment. The marker is appended to the body so
|
|
313
|
+
* the check above can see it. */
|
|
314
|
+
export async function commentIssueOnce(cfg, issueNumber, body, key) {
|
|
315
|
+
if (await issueHasCommentMarker(cfg, issueNumber, key))
|
|
316
|
+
return false;
|
|
317
|
+
await commentIssue(cfg, issueNumber, `${body}\n\n${bivyCommentMarker(key)}`);
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
291
320
|
/**
|
|
292
321
|
* The comment posted on an issue the moment Bivy picks it up — the "visibly
|
|
293
322
|
* signal pickup" half of the on-issue lifecycle (the other half is the label
|
|
@@ -318,7 +347,9 @@ export async function announcePickup(cfg, issueNumber, nodeName) {
|
|
|
318
347
|
if (cfg.label && cfg.label !== cfg.claimLabel) {
|
|
319
348
|
await removeLabel(cfg, issueNumber, cfg.label).catch((error) => warn(`remove routing label "${cfg.label}"`, error));
|
|
320
349
|
}
|
|
321
|
-
|
|
350
|
+
// Idempotent across reclaims: a Machine that reclaims this issue after another
|
|
351
|
+
// lost its lease must not post a second pickup comment.
|
|
352
|
+
await commentIssueOnce(cfg, issueNumber, pickupMessage(nodeName), "pickup").catch((error) => warn("post pickup comment", error));
|
|
322
353
|
}
|
|
323
354
|
export async function defaultBranch(cfg) {
|
|
324
355
|
const res = await gh(cfg, "GET", "");
|
package/dist/server.js
CHANGED
|
@@ -78,7 +78,7 @@ import { setConfiguredAutoAttachToolImages } from "./harness/tool-image-attachme
|
|
|
78
78
|
import { injectMcpProxyForSession, injectBivyToolsForSession } from "./harness/mcp-inject.js";
|
|
79
79
|
import { parseRepo, isGitHubSlugPart, inferGitHubRepoFromWorkspace, isSharedCloneRoot, resolveGitHubToken, ghCliInstalled, cloneOrUpdateRepo, resolveDefaultBaseRef, resolveBranchBaseRef, resolveAdoptBaseRef, fetchOrigin } from "./repo-workspace.js";
|
|
80
80
|
import { configureGitAuth, writeGitCredentialEndpoint } from "./git-auth.js";
|
|
81
|
-
import { GitHubTaskPoller, resolveGitHubTaskConfig, buildTaskPrompt, buildResumePrompt, buildInteractiveResumePrompt, DEFAULT_ISSUE_INSTRUCTIONS, parseBivyDirectives, commitAll, pushBranch, mergeBaseIntoBranch, completeMerge, abortMerge, findOpenPullRequestForBranch, findPullRequestsForBranch, findMergedPullRequestForBranch, issueBranchName, getPullRequest,
|
|
81
|
+
import { GitHubTaskPoller, resolveGitHubTaskConfig, buildTaskPrompt, buildResumePrompt, buildInteractiveResumePrompt, DEFAULT_ISSUE_INSTRUCTIONS, parseBivyDirectives, commitAll, pushBranch, mergeBaseIntoBranch, completeMerge, abortMerge, findOpenPullRequestForBranch, findPullRequestsForBranch, findMergedPullRequestForBranch, issueBranchName, getPullRequest, commentIssueOnce, listOpenLabelledIssues, selectActionableIssues, getIssue, getIssueCommentBody, addLabel, removeLabel, announcePickup, } from "./github-tasks.js";
|
|
82
82
|
import { buildLinearTaskPrompt, getLinearIssue, linearBranchName } from "./linear-tasks.js";
|
|
83
83
|
import { PairingStore } from "./device-registry.js";
|
|
84
84
|
import { IntegrationManager } from "./integrations/index.js";
|
|
@@ -4185,7 +4185,9 @@ async function reportIssueOutcome(cfg, issue, record, emit, opts) {
|
|
|
4185
4185
|
await prDetection.maybeDetectPullRequest(record);
|
|
4186
4186
|
if (record.prUrl) {
|
|
4187
4187
|
emit(record, "pr_opened", `Pull request ready for issue #${issue.number}.`, { prUrl: record.prUrl });
|
|
4188
|
-
|
|
4188
|
+
// Keyed by the PR URL so a reclaim/retry that lands on the same PR does not
|
|
4189
|
+
// post a second link, while a genuinely new PR still gets its own comment.
|
|
4190
|
+
await commentIssueOnce(cfg, issue.number, `🤖 ${record.prUrl}`, `pr:${record.prUrl}`).catch(() => { });
|
|
4189
4191
|
// Clean up the claim label now that the PR itself is the live "in progress"
|
|
4190
4192
|
// signal on the issue (linked in the timeline + the comment above) — keeping
|
|
4191
4193
|
// `bivy:in-progress` around after a PR exists is stale and, per the issue's
|
|
@@ -4199,7 +4201,9 @@ async function reportIssueOutcome(cfg, issue, record, emit, opts) {
|
|
|
4199
4201
|
? "Bivy handled the follow-up but produced no file changes."
|
|
4200
4202
|
: "Bivy ran on this issue but produced no file changes.";
|
|
4201
4203
|
emit(record, "no_changes", message);
|
|
4202
|
-
|
|
4204
|
+
// Keyed by the deterministic issue branch so a reclaim doesn't repeat the
|
|
4205
|
+
// no-changes note for the same attempt cycle.
|
|
4206
|
+
await commentIssueOnce(cfg, issue.number, message, `no-changes:${wt.branch}`).catch(() => { });
|
|
4203
4207
|
// The run is finished with nothing in progress — drop the claim label so it
|
|
4204
4208
|
// doesn't linger on the issue. A stale `bivy/<node>:in-progress` label was
|
|
4205
4209
|
// also mis-routing follow-up mentions before pickRoutingLabel was hardened;
|
|
@@ -4208,7 +4212,7 @@ async function reportIssueOutcome(cfg, issue, record, emit, opts) {
|
|
|
4208
4212
|
return;
|
|
4209
4213
|
}
|
|
4210
4214
|
emit(record, "pushed", `Pushed ${wt.branch} for issue #${issue.number}; no pull request yet.`);
|
|
4211
|
-
await
|
|
4215
|
+
await commentIssueOnce(cfg, issue.number, `🤖 Pushed \`${wt.branch}\` but didn't open a pull request. Comment \`@bivy\` again to continue, or open one from the session's chat (\`/pr\`).`, `pushed:${wt.branch}`).catch(() => { });
|
|
4212
4216
|
}
|
|
4213
4217
|
/** The live open session for an issue (by source tag), if we still have one. */
|
|
4214
4218
|
function findIssueSession(source) {
|
|
@@ -6257,6 +6261,12 @@ function evictSessionRecord(record, reason) {
|
|
|
6257
6261
|
function detachSessionRecord(record, reason) {
|
|
6258
6262
|
persistSessionMetadata(record, "idle");
|
|
6259
6263
|
eventLog.flush(record.id);
|
|
6264
|
+
// Evict the in-memory overlay/maps for the detached session (the child stays
|
|
6265
|
+
// alive on the service, and a re-attach lazily reloads the log from disk).
|
|
6266
|
+
// Keeps a churn of detach/re-attach from leaking like close did.
|
|
6267
|
+
eventLog.drop(record.id);
|
|
6268
|
+
mcpInventoryBySession.delete(record.id);
|
|
6269
|
+
eventLogIssues.delete(record.id);
|
|
6260
6270
|
questionManager.cancelForSession(record.id);
|
|
6261
6271
|
approvals.cancelForSession(record.id);
|
|
6262
6272
|
record.unsubscribe?.();
|
|
@@ -6282,6 +6292,12 @@ function closeSessionRecord(record, reason = "closed") {
|
|
|
6282
6292
|
void sessionTerminals.forget(record.id).catch(() => { });
|
|
6283
6293
|
persistSessionMetadata(record, "idle");
|
|
6284
6294
|
eventLog.flush(record.id);
|
|
6295
|
+
// Evict the flushed session's in-memory overlay so a long-lived daemon doesn't
|
|
6296
|
+
// retain every session it ever opened. drop() only clears the in-memory maps
|
|
6297
|
+
// and cancels the pending timer — the on-disk JSONL stays, and a reopen lazily
|
|
6298
|
+
// reloads it via load(). Without this the EventLog.disk cache grew monotonically
|
|
6299
|
+
// (only deleteSessionFile dropped it), the standout non-recovering leak.
|
|
6300
|
+
eventLog.drop(record.id);
|
|
6285
6301
|
// Cancel any question still awaiting an answer so its card closes and the
|
|
6286
6302
|
// guardian promise (and the tool call behind it) settles rather than hanging
|
|
6287
6303
|
// until timeout. Belt-and-suspenders alongside the tool-call abort signal.
|
|
@@ -6308,6 +6324,10 @@ function closeSessionRecord(record, reason = "closed") {
|
|
|
6308
6324
|
if (record.sessionFile)
|
|
6309
6325
|
openSessions.delete(path.resolve(record.sessionFile));
|
|
6310
6326
|
lastRecordedCostUsd.delete(record.id);
|
|
6327
|
+
// Per-session maps that were only ever populated, never pruned — evict on close
|
|
6328
|
+
// so they don't accumulate for the daemon's lifetime.
|
|
6329
|
+
mcpInventoryBySession.delete(record.id);
|
|
6330
|
+
eventLogIssues.delete(record.id);
|
|
6311
6331
|
if (active?.id === record.id)
|
|
6312
6332
|
active = undefined;
|
|
6313
6333
|
broadcast({ type: "session.closed", sessionId: record.id, sessionFile: record.sessionFile, reason });
|
package/package.json
CHANGED