@bivy/bivy 0.10.1-staging.164 → 0.10.1-staging.166
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/server.js +89 -15
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -5462,17 +5462,55 @@ function linearSessionSource(externalId) {
|
|
|
5462
5462
|
* starting, and return false so the caller falls through. Returns true only when
|
|
5463
5463
|
* it fully handled the item.
|
|
5464
5464
|
*/
|
|
5465
|
-
async function continueCorrelatedSession(item, prompt, report) {
|
|
5465
|
+
async function continueCorrelatedSession(item, prompt, report, opts) {
|
|
5466
5466
|
if (item.targetKind !== "existing_session" || !item.targetSessionId)
|
|
5467
5467
|
return false;
|
|
5468
|
-
|
|
5468
|
+
let record = openSessions.get(item.targetSessionId);
|
|
5469
|
+
if (!record && opts?.resumeOnMissing) {
|
|
5470
|
+
// Same-node resume: reopen a closed session from its durable metadata +
|
|
5471
|
+
// transcript — exactly how a prompt to an old session from the app is
|
|
5472
|
+
// handled (resolveOrResumeSession). Best-effort: an unresolvable session
|
|
5473
|
+
// falls through to the snapshot restore / fail handling below.
|
|
5474
|
+
record = await resolveOrResumeSession(item.targetSessionId).catch(() => undefined);
|
|
5475
|
+
}
|
|
5469
5476
|
if (!record) {
|
|
5470
5477
|
await restoreSessionFromSnapshot(item.targetSessionId).catch((e) => console.warn(`[case-b] snapshot restore for ${item.targetSessionId} failed:`, e.message));
|
|
5478
|
+
record = openSessions.get(item.targetSessionId);
|
|
5479
|
+
}
|
|
5480
|
+
if (!record) {
|
|
5481
|
+
// Strict mode (a scheduled message to a specific session): the session must
|
|
5482
|
+
// be resumed in place, so a genuinely-unavailable one is surfaced as a
|
|
5483
|
+
// failed run rather than silently starting a new session. Everything else
|
|
5484
|
+
// keeps the established best-effort fall-through to a fresh pickup.
|
|
5485
|
+
if (opts?.resumeOnMissing) {
|
|
5486
|
+
throw new Error(`Scheduled message could not be delivered: session ${item.targetSessionId} is not available on this node`);
|
|
5487
|
+
}
|
|
5471
5488
|
return false;
|
|
5472
5489
|
}
|
|
5473
5490
|
const branch = record.worktree?.branch;
|
|
5491
|
+
if (opts?.resumeOnMissing) {
|
|
5492
|
+
// "Send when the turn ends": a scheduled message that fires mid-turn waits
|
|
5493
|
+
// for the session to go idle instead of interrupting it (bounded, so a
|
|
5494
|
+
// stuck session still surfaces as a failed run rather than hanging forever).
|
|
5495
|
+
await waitForSessionIdle(record);
|
|
5496
|
+
// Double-send guard: when the app is open and already delivered this exact
|
|
5497
|
+
// message as a follow-up (and deleted the pending schedule), the scheduled
|
|
5498
|
+
// run must not send it a second time. Matches the last user message, which
|
|
5499
|
+
// is all a text-only scheduled message can have produced.
|
|
5500
|
+
if (lastUserMessageText(record).trim() === prompt.trim()) {
|
|
5501
|
+
await report({
|
|
5502
|
+
output: { sessionId: record.id },
|
|
5503
|
+
events: [{
|
|
5504
|
+
at: new Date().toISOString(),
|
|
5505
|
+
kind: "completed",
|
|
5506
|
+
summary: "Already delivered to this session — skipped.",
|
|
5507
|
+
}],
|
|
5508
|
+
});
|
|
5509
|
+
return true;
|
|
5510
|
+
}
|
|
5511
|
+
}
|
|
5474
5512
|
await runSessionTurn(record, prompt);
|
|
5475
|
-
if (record.worktree) {
|
|
5513
|
+
if (record.worktree && !opts?.isMessage && !opts?.resumeOnMissing) {
|
|
5476
5514
|
await maybePushWorktreeBranch(record);
|
|
5477
5515
|
await maybeDetectPullRequest(record);
|
|
5478
5516
|
}
|
|
@@ -5484,6 +5522,27 @@ async function continueCorrelatedSession(item, prompt, report) {
|
|
|
5484
5522
|
});
|
|
5485
5523
|
return true;
|
|
5486
5524
|
}
|
|
5525
|
+
/** Poll a session until it's not working/streaming (scheduled-message idle wait). */
|
|
5526
|
+
async function waitForSessionIdle(record, capMs = 2 * 60 * 60 * 1000) {
|
|
5527
|
+
const deadline = Date.now() + capMs;
|
|
5528
|
+
while (sessionBusy(record)) {
|
|
5529
|
+
if (Date.now() > deadline) {
|
|
5530
|
+
throw new Error(`Timed out waiting for session ${record.id} to finish its current turn`);
|
|
5531
|
+
}
|
|
5532
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
5533
|
+
}
|
|
5534
|
+
}
|
|
5535
|
+
/** Text of the most recent user message in a session's transcript, if any. */
|
|
5536
|
+
function lastUserMessageText(record) {
|
|
5537
|
+
const messages = record.session.getMessages();
|
|
5538
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
5539
|
+
const message = messages[i];
|
|
5540
|
+
if (message && typeof message === "object" && message.role === "user") {
|
|
5541
|
+
return runtimeContentText(message.content);
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
return "";
|
|
5545
|
+
}
|
|
5487
5546
|
async function runWorkItem(item, report) {
|
|
5488
5547
|
// Scheduled, manual, and webhook-triggered automations carry the operator's
|
|
5489
5548
|
// instructions as an E2E template (`bivy-room-v1:<node>:<ciphertext>`) only the
|
|
@@ -5617,12 +5676,25 @@ async function runWorkItem(item, report) {
|
|
|
5617
5676
|
const parsedRepo = item.repo ? parseRepo(item.repo) : undefined;
|
|
5618
5677
|
if (item.repo && !parsedRepo)
|
|
5619
5678
|
throw new Error(`work item ${item.id} has an invalid repo "${item.repo}"`);
|
|
5620
|
-
|
|
5679
|
+
// A scheduled run's body is the operator's (decrypted) template — the
|
|
5680
|
+
// definition name is metadata, not part of the instructions, so don't fold it
|
|
5681
|
+
// into the prompt. A scheduled chat message must arrive verbatim; the name is
|
|
5682
|
+
// only the run/session label.
|
|
5683
|
+
const request = item.source === "schedule"
|
|
5684
|
+
? (item.body || item.title)
|
|
5685
|
+
: item.body ? `${item.title}\n\n${item.body}` : item.title;
|
|
5686
|
+
// Plain chat messages (scheduled "message me later" reminders) skip the
|
|
5687
|
+
// automation boilerplate, auto-push and required checks — even when a
|
|
5688
|
+
// workspace target happens to be a git checkout.
|
|
5689
|
+
const isMessage = item.message === true;
|
|
5621
5690
|
// Case B (provider-agnostic): a follow-up the control plane correlated to an
|
|
5622
5691
|
// existing session continues it as a normal chat. Reached by Slack the moment a
|
|
5623
5692
|
// reply carries a thread identity the control plane can correlate; a one-shot
|
|
5624
5693
|
// slash command has none, so it simply falls through to a fresh session.
|
|
5625
|
-
|
|
5694
|
+
// Scheduled runs targeting an existing session are STRICT: the message must
|
|
5695
|
+
// land in that session (resumed from disk if needed), never silently in a new
|
|
5696
|
+
// one — so a session that can't be resumed fails the run instead.
|
|
5697
|
+
if (await continueCorrelatedSession(item, request, report, { resumeOnMissing: item.source === "schedule", isMessage }))
|
|
5626
5698
|
return;
|
|
5627
5699
|
const sandbox = normalizeSandboxTier(item.sandbox);
|
|
5628
5700
|
const sessionOpts = {
|
|
@@ -5645,16 +5717,18 @@ async function runWorkItem(item, report) {
|
|
|
5645
5717
|
}
|
|
5646
5718
|
catch { }
|
|
5647
5719
|
}
|
|
5648
|
-
const prompt =
|
|
5649
|
-
?
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5720
|
+
const prompt = isMessage
|
|
5721
|
+
? request
|
|
5722
|
+
: (parsedRepo || record.worktree)
|
|
5723
|
+
? [
|
|
5724
|
+
request,
|
|
5725
|
+
"",
|
|
5726
|
+
"Work carefully in this repository and implement the request completely. Run the relevant tests, linter, and type-checker.",
|
|
5727
|
+
"When finished, commit and push your changes, then open a pull request with a clear title and description. If you cannot open it, leave the changes committed on this branch.",
|
|
5728
|
+
].join("\n")
|
|
5729
|
+
: request;
|
|
5656
5730
|
await runSessionTurn(record, prompt);
|
|
5657
|
-
if (record.worktree) {
|
|
5731
|
+
if (!isMessage && record.worktree) {
|
|
5658
5732
|
await maybePushWorktreeBranch(record);
|
|
5659
5733
|
await maybeDetectPullRequest(record);
|
|
5660
5734
|
}
|
|
@@ -5664,7 +5738,7 @@ async function runWorkItem(item, report) {
|
|
|
5664
5738
|
// because the agent claimed success or already opened a PR. Only a run that was
|
|
5665
5739
|
// allowed to modify the workspace is verified; a read-only run (an
|
|
5666
5740
|
// investigation) changes nothing, so there is nothing to check.
|
|
5667
|
-
const checks = record.worktree && sandbox !== "read-only"
|
|
5741
|
+
const checks = !isMessage && record.worktree && sandbox !== "read-only"
|
|
5668
5742
|
? runRequiredAutomationChecks(record.worktree.path)
|
|
5669
5743
|
: [];
|
|
5670
5744
|
const failedChecks = checks.filter((check) => check.status === "failed");
|
package/package.json
CHANGED