@cmgmyr/hive 1.11.6 → 1.11.7
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/build-info.json +1 -1
- package/dist/claudeWake.js +52 -4
- package/dist/cli.js +66 -41
- package/dist/crew.js +4 -3
- package/dist/dashboard.js +6 -5
- package/dist/etime.js +37 -0
- package/dist/firstPrompt.js +6 -0
- package/dist/heldLabel.js +1 -1
- package/dist/leadHandoff.js +12 -11
- package/dist/leadHandoffCli.js +22 -1
- package/dist/leadHandoffState.js +1 -2
- package/dist/leadMessage.js +13 -0
- package/dist/leadMessageDelivery.js +14 -74
- package/dist/leadState.js +3 -2
- package/dist/leaseStore.js +54 -0
- package/dist/portfolio.js +5 -2
- package/dist/ptys.js +2 -9
- package/dist/queenDashboard.js +2 -1
- package/dist/scheduler.js +40 -166
- package/dist/staleServers.js +4 -28
- package/dist/stateProvenance.js +1 -3
- package/dist/storeClock.js +4 -0
- package/dist/teardown.js +2 -1
- package/dist/tools/leases.js +5 -25
- package/dist/tools/wakes.js +7 -15
- package/dist/wakeSql.js +135 -1
- package/package.json +1 -1
package/dist/scheduler.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { QUEEN_AUDIT_RETENTION, QUEEN_AUDIT_MAX_ROWS, recentQueenAudit } from "./queenAudit.js";
|
|
2
|
-
import { ACTIVE_TIMER_WHERE } from "./wakeSql.js";
|
|
3
|
-
export { ACTIVE_TIMER_WHERE } from "./wakeSql.js";
|
|
2
|
+
import { ACTIVE_TIMER_WHERE, DELIVERY_METHOD, FIRING_DROPPED_NOTE, HELD_REASON_ACTOR_CLOSED, HELD_REASON_CONVERSATION, HELD_REASON_COPY_MODE, HELD_REASON_HANDOFF, HELD_REASON_LEAD_BOOT_INPUT_BOX, HELD_REASON_LEAD_PANE_DEAD, HELD_REASON_LEAD_ROW_CLOSED, HELD_REASON_MODAL_CHOICE, HELD_REASON_OWNERSHIP_LOST_AFTER_CLAIM, HELD_REASON_PANE_REISSUED_LEAD, HELD_REASON_PANE_REISSUED_WORKER, HELD_REASON_PANE_UNKNOWN_LEAD, HELD_REASON_PANE_UNKNOWN_WORKER, HELD_REASON_UNCLASSIFIABLE_PANE, HELD_REASON_UNSUBMITTED_INPUT, HELD_REASON_UNSUBMITTED_INPUT_OWNER_UNRESOLVED, LOG_RETENTION, activeWakeWhere, holdableWakeWhere, identityThenDead, pendingOneShotWhere, repeatingFiringPendingWhere, repeatingSocketAwaitingWhere, repeatingSocketFiringWhere, schedulerCandidateWhere, sentWakeAtSql, socketAwaitingWhere, spentUntypedNotice, wasHeldForPaneIdentity, watchWatchingWhere, } from "./wakeSql.js";
|
|
3
|
+
export { ACTIVE_TIMER_WHERE, DELIVERY_METHOD, HELD_REASON_ACTOR_CLOSED, HELD_REASON_CONVERSATION, HELD_REASON_COPY_MODE, HELD_REASON_LEAD_PANE_DEAD, HELD_REASON_UNCLASSIFIABLE_PANE, HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX, HELD_REASON_UNSUBMITTED_INPUT, HELD_REASON_UNSUBMITTED_INPUT_PREFIX, isLeadRowClosedHold, isUnclassifiablePaneHold, isUnsubmittedInputHold, socketAwaitingWhere, wasHeldForPaneIdentity, wasHeldForPaneReissue, } from "./wakeSql.js";
|
|
4
|
+
import { parseStoreTimestamp } from "./storeClock.js";
|
|
4
5
|
import { runningBuildChange, runningBuildNotice, versionInfo } from "./version.js";
|
|
5
6
|
import { existsSync, lstatSync, mkdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
7
|
import { createHash } from "node:crypto";
|
|
@@ -17,13 +18,14 @@ import { QUEEN_GENERATED_MARKER, readQueenBrief, renderQueenDashboard } from "./
|
|
|
17
18
|
import { COMMAND_KIND, stoppingMarkerLive } from "./processes.js";
|
|
18
19
|
import { closeAgentRow, PaneOwnershipLost, reapCodexHomeForClosedAgent, requireStillOwned, } from "./spawn.js";
|
|
19
20
|
import { isLeadActorId, LEAD_ACTOR_PREFIX, LEAD_KIND, } from "./agentKinds.js";
|
|
20
|
-
import { awaitingFirstPrompt,
|
|
21
|
+
import { awaitingFirstPrompt, finishedIdle, finishedIdleSql } from "./firstPrompt.js";
|
|
21
22
|
import { evaluateLeadWatch, leadSubscription } from "./leadWatch.js";
|
|
22
23
|
import { describeLiveTasks, describeOneTask, liveBackgroundTasks, STALL_BOUND_SECONDS, } from "./backgroundTasks.js";
|
|
23
24
|
import { MESSAGE_MAX_ROWS, MESSAGE_RETENTION } from "./leadMessage.js";
|
|
24
|
-
import {
|
|
25
|
+
import { PANE_LEASE_SETTLE_MS, releasePaneLease, tryAcquirePaneLease } from "./leaseStore.js";
|
|
26
|
+
import { CROSS_SESSION_CLOSE, postClaudeWake, promptMarkerEvidenceSql, quietMessagingOn, socketGraceDue, socketSenderAddress, SOCKET_WAKE_FOOTER, } from "./claudeWake.js";
|
|
25
27
|
import { driveLeadHandoff } from "./leadHandoff.js";
|
|
26
|
-
import { handoffHoldsWake
|
|
28
|
+
import { handoffHoldsWake } from "./leadHandoffState.js";
|
|
27
29
|
import { FIRST_MESSAGE_LOG_STATE, humanPromptSql, readLeadTurnState } from "./leadState.js";
|
|
28
30
|
import { retryQuietLeadMessages } from "./leadMessageDelivery.js";
|
|
29
31
|
import { harnessFor, hasTranscriptSignal, paneClassifierFor, screenClassifiable, transcriptDirFor } from "./harnesses.js";
|
|
@@ -282,7 +284,7 @@ function cancelTimer(timerId) {
|
|
|
282
284
|
function cancelWithReason(timerId, reason) {
|
|
283
285
|
bestEffortRun("UPDATE wakes SET cancelled_at = datetime('now'), held_reason = ? WHERE id = ? AND cancelled_at IS NULL", reason, timerId);
|
|
284
286
|
}
|
|
285
|
-
export
|
|
287
|
+
export { LOG_RETENTION } from "./wakeSql.js";
|
|
286
288
|
const LOG_MAX_ROWS = 20_000;
|
|
287
289
|
function pruneStateLog() {
|
|
288
290
|
try {
|
|
@@ -470,7 +472,7 @@ function maybeGenerateDashboards() {
|
|
|
470
472
|
}
|
|
471
473
|
}
|
|
472
474
|
export function checkConfirmations() {
|
|
473
|
-
const sentAt = "
|
|
475
|
+
const sentAt = sentWakeAtSql("wakes.");
|
|
474
476
|
const confirmationQuery = promptEvidenceSql(sentAt);
|
|
475
477
|
try {
|
|
476
478
|
const pending = stmt(`SELECT 1 AS hit FROM wakes
|
|
@@ -542,11 +544,7 @@ export async function tick(snapshot) {
|
|
|
542
544
|
const now = stmt("SELECT datetime('now') AS now").get().now;
|
|
543
545
|
const candidates = stmt(`SELECT wakes.*, ${DELIVER_ROW_COLUMNS}
|
|
544
546
|
FROM wakes ${DELIVER_SOCKET_JOIN}
|
|
545
|
-
WHERE wakes.
|
|
546
|
-
(wakes.kind = 'delay' AND wakes.due_at <= datetime('now')
|
|
547
|
-
AND (wakes.fired_at IS NULL OR wakes.repeat_every_ms IS NOT NULL))
|
|
548
|
-
OR (wakes.kind != 'delay' AND wakes.fired_at IS NULL)
|
|
549
|
-
)`).all();
|
|
547
|
+
WHERE ${schedulerCandidateWhere("wakes.")}`).all();
|
|
550
548
|
const choices = new Map();
|
|
551
549
|
for (const timer of candidates) {
|
|
552
550
|
await isolated(`timer ${timer.id}`, async () => {
|
|
@@ -622,82 +620,14 @@ function holdTimer(timer, reason) {
|
|
|
622
620
|
const count = timer.fire_count ?? null;
|
|
623
621
|
bestEffortRun(`UPDATE wakes SET held_at = datetime('now'), held_reason = ?,
|
|
624
622
|
first_held_at = COALESCE(CASE WHEN held_at IS NULL THEN NULL ELSE first_held_at END, datetime('now'))
|
|
625
|
-
WHERE id = ? AND
|
|
626
|
-
AND (fired_at IS NULL OR (repeat_every_ms IS NOT NULL AND due_at = ? AND (? IS NULL OR fire_count = ?))
|
|
627
|
-
OR (${socketAwaitingWhere()}))`, reason, timer.id, timer.due_at, count, count);
|
|
623
|
+
WHERE id = ? AND ${holdableWakeWhere()}`, reason, timer.id, timer.due_at, count, count);
|
|
628
624
|
}
|
|
629
|
-
const HELD_REASON_MODAL_CHOICE = "pane is awaiting a modal choice (folder-trust or /model picker)";
|
|
630
625
|
const LEAD_BOOT_BOX_WAIT_MS = 60_000;
|
|
631
|
-
const HELD_REASON_LEAD_BOOT_INPUT_BOX = "the lead's input box is not on screen yet; holding while its session starts";
|
|
632
|
-
export const HELD_REASON_LEAD_PANE_DEAD = "the lead's pane is not live right now (likely mid-restart); lead-owned wakes are exempt from " +
|
|
633
|
-
"cancellation for this alone, so it is held rather than lost";
|
|
634
|
-
const HELD_REASON_PANE_REISSUED_PREFIX = "the pane id recorded for this wake now belongs to a different pane than the one it was set " +
|
|
635
|
-
"against (its pid no longer matches, most likely a tmux server restart reissuing the id); held " +
|
|
636
|
-
"rather than typed into the wrong pane - ";
|
|
637
|
-
const HELD_REASON_PANE_REISSUED_LEAD = `${HELD_REASON_PANE_REISSUED_PREFIX}run \`hive lead\` to re-point it at the live one`;
|
|
638
|
-
const HELD_REASON_PANE_REISSUED_WORKER = `${HELD_REASON_PANE_REISSUED_PREFIX}nothing re-points a worker's wake automatically, so cancel ` +
|
|
639
|
-
"it with wake_cancel and set a fresh one once the worker's pane is confirmed live, or leave it: " +
|
|
640
|
-
"it will keep holding rather than deliver wrongly";
|
|
641
|
-
const HELD_REASON_PANE_REISSUED_THEN_DEAD = `${HELD_REASON_PANE_REISSUED_PREFIX}the pane it was reissued to has since gone dead too; nothing ` +
|
|
642
|
-
"re-points a worker's wake automatically, so cancel it with wake_cancel (any running lead may do " +
|
|
643
|
-
"this even though the wake is not theirs) if it is no longer needed";
|
|
644
|
-
export function wasHeldForPaneReissue(heldReason) {
|
|
645
|
-
return heldReason != null && heldReason.startsWith(HELD_REASON_PANE_REISSUED_PREFIX);
|
|
646
|
-
}
|
|
647
|
-
const HELD_REASON_PANE_UNKNOWN_PREFIX = "pane ownership for this wake's row cannot be verified (no pane pid was recorded for it, its row now " +
|
|
648
|
-
"records a different pane, or the pane is on another tmux socket); held rather than typed into a pane " +
|
|
649
|
-
"that may not be its own - ";
|
|
650
|
-
const HELD_REASON_PANE_UNKNOWN_LEAD = `${HELD_REASON_PANE_UNKNOWN_PREFIX}run \`hive lead\` to start or adopt a lead with a recorded pane and re-point it`;
|
|
651
|
-
const HELD_REASON_PANE_UNKNOWN_WORKER = `${HELD_REASON_PANE_UNKNOWN_PREFIX}nothing re-points a worker's wake automatically, so cancel it with ` +
|
|
652
|
-
"wake_cancel (any running lead may, even though the wake is not theirs), or leave it: it keeps holding " +
|
|
653
|
-
"rather than deliver wrongly";
|
|
654
|
-
const HELD_REASON_PANE_UNKNOWN_THEN_DEAD = `${HELD_REASON_PANE_UNKNOWN_PREFIX}that pane has since gone dead too; nothing re-points a worker's wake ` +
|
|
655
|
-
"automatically, so cancel it with wake_cancel (any running lead may) if it is no longer needed";
|
|
656
|
-
// One family for both identity holds, so a later dead pane re-holds them instead of cancelling.
|
|
657
|
-
export function wasHeldForPaneIdentity(heldReason) {
|
|
658
|
-
return wasHeldForPaneReissue(heldReason) || (heldReason?.startsWith(HELD_REASON_PANE_UNKNOWN_PREFIX) ?? false);
|
|
659
|
-
}
|
|
660
|
-
const identityThenDead = (heldReason) => wasHeldForPaneReissue(heldReason) ? HELD_REASON_PANE_REISSUED_THEN_DEAD : HELD_REASON_PANE_UNKNOWN_THEN_DEAD;
|
|
661
|
-
export const HELD_REASON_ACTOR_CLOSED = "delivery actor closed: the row this wake was bound to is closed, so nothing was typed and the wake was " +
|
|
662
|
-
"cancelled rather than delivered into a pane that row no longer owns";
|
|
663
|
-
const HELD_REASON_LEAD_ROW_CLOSED = "the lead's row is closed, so no pane is this wake's to type into; held until `hive lead` starts or adopts a " +
|
|
664
|
-
"lead and re-points it";
|
|
665
|
-
export function isLeadRowClosedHold(heldReason) {
|
|
666
|
-
return heldReason === HELD_REASON_LEAD_ROW_CLOSED;
|
|
667
|
-
}
|
|
668
|
-
const HELD_REASON_OWNERSHIP_LOST_AFTER_CLAIM = "delivery stopped after claim: ";
|
|
669
|
-
export const HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX = "the pane's harness is not one hive can classify; ";
|
|
670
|
-
export const HELD_REASON_UNCLASSIFIABLE_PANE = `${HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX}neither the dialog guard nor the unsubmitted-text ` +
|
|
671
|
-
"guard can read this screen and delivery would type blind. wake_set and wake_when_idle refuse such a " +
|
|
672
|
-
"target outright now, so this wake predates that or its pane's command changed underneath it. Nothing " +
|
|
673
|
-
"hive does on its own clears this: it lifts only when that pane is replaced by one hive starts itself " +
|
|
674
|
-
"(`hive lead` after the pane exits). Cancel it with wake_cancel (any running lead may, even though the " +
|
|
675
|
-
"wake is not theirs) and drive that pane with agent_send(keys: [...])";
|
|
676
|
-
export function isUnclassifiablePaneHold(heldReason) {
|
|
677
|
-
return heldReason != null && heldReason.startsWith(HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX);
|
|
678
|
-
}
|
|
679
|
-
export const HELD_REASON_COPY_MODE = "the pane is in tmux copy mode; tmux clears its bracketed-paste flag there, so the body would arrive " +
|
|
680
|
-
"with no paste markers (losing everything before the last 1022-byte write) and the Enter would be " +
|
|
681
|
-
"eaten by the mode instead of submitting - both silently, with tmux reporting success. It delivers " +
|
|
682
|
-
"once whoever is reading that pane leaves copy mode";
|
|
683
|
-
export const HELD_REASON_UNSUBMITTED_INPUT_PREFIX = "the pane's input box has unsubmitted human text; ";
|
|
684
|
-
export const HELD_REASON_UNSUBMITTED_INPUT = `${HELD_REASON_UNSUBMITTED_INPUT_PREFIX}delivering now would paste the wake body onto it and submit ` +
|
|
685
|
-
"both as one message";
|
|
686
|
-
// Distinct from HELD_REASON_UNSUBMITTED_INPUT so the claim's debounce doesn't latch a transient
|
|
687
|
-
// pane-resolution failure shut forever - the next tick retries instead of giving up permanently.
|
|
688
|
-
const HELD_REASON_UNSUBMITTED_INPUT_OWNER_UNRESOLVED = `${HELD_REASON_UNSUBMITTED_INPUT_PREFIX}hive could not resolve a live pane for the wake's owner on ` +
|
|
689
|
-
"this tick - retrying, since that may be transient rather than permanent";
|
|
690
|
-
export function isUnsubmittedInputHold(heldReason) {
|
|
691
|
-
return heldReason != null && heldReason.startsWith(HELD_REASON_UNSUBMITTED_INPUT_PREFIX);
|
|
692
|
-
}
|
|
693
|
-
export const HELD_REASON_CONVERSATION = "a human talked to this lead more recently than the conversation-hold window; holding so a wake " +
|
|
694
|
-
"does not split an in-progress discussion - it delivers once the window passes or the hold's own " +
|
|
695
|
-
"ceiling is reached, whichever comes first";
|
|
696
626
|
function leadBootSessionIsFresh(agentId, panePid) {
|
|
697
627
|
const row = stmt("SELECT pane_started_at FROM agents WHERE id = ? AND pane_pid = ?").get(agentId, panePid);
|
|
698
628
|
if (!row?.pane_started_at)
|
|
699
629
|
return false;
|
|
700
|
-
const timestamp =
|
|
630
|
+
const timestamp = parseStoreTimestamp(row.pane_started_at);
|
|
701
631
|
return Number.isFinite(timestamp) && Date.now() - timestamp < LEAD_BOOT_BOX_WAIT_MS;
|
|
702
632
|
}
|
|
703
633
|
function leadInputBoxObservation(pane, command, cache) {
|
|
@@ -828,7 +758,7 @@ function insertNotice(timer, deliverActor, pane, body, parentTimerId) {
|
|
|
828
758
|
function pendingNoticeFor(parentTimerId, conditions) {
|
|
829
759
|
const placeholders = conditions.map(() => "?").join(", ");
|
|
830
760
|
return stmt(`SELECT t.id FROM wakes t
|
|
831
|
-
WHERE t.parent_wake_id = ? AND t.
|
|
761
|
+
WHERE t.parent_wake_id = ? AND ${pendingOneShotWhere("t.")}
|
|
832
762
|
AND EXISTS (
|
|
833
763
|
SELECT 1 FROM wake_idle_notices n WHERE n.notice_wake_id = t.id AND n.condition IN (${placeholders})
|
|
834
764
|
)
|
|
@@ -836,7 +766,7 @@ function pendingNoticeFor(parentTimerId, conditions) {
|
|
|
836
766
|
}
|
|
837
767
|
function updateNoticeInPlace(noticeId, body) {
|
|
838
768
|
return (stmt(`UPDATE wakes SET body = ?, created_at = datetime('now')
|
|
839
|
-
WHERE id = ? AND
|
|
769
|
+
WHERE id = ? AND ${pendingOneShotWhere()}`).run(body, noticeId).changes === 1);
|
|
840
770
|
}
|
|
841
771
|
function claimBlockNotice(timerId, agentId, blockedSince) {
|
|
842
772
|
return (stmt(`INSERT OR IGNORE INTO wake_block_notices (wake_id, agent_id, blocked_since)
|
|
@@ -845,9 +775,8 @@ function claimBlockNotice(timerId, agentId, blockedSince) {
|
|
|
845
775
|
const claimModalHoldWithNotice = db.transaction((timer, pane, body, target) => {
|
|
846
776
|
const claimed = stmt(`UPDATE wakes SET held_at = datetime('now'), held_reason = ?,
|
|
847
777
|
first_held_at = COALESCE(CASE WHEN held_at IS NULL THEN NULL ELSE first_held_at END, datetime('now'))
|
|
848
|
-
WHERE id = ? AND
|
|
778
|
+
WHERE id = ? AND (${activeWakeWhere()})
|
|
849
779
|
AND due_at IS ? AND body IS ? AND repeat_every_ms IS ?
|
|
850
|
-
AND (fired_at IS NULL OR repeat_every_ms IS NOT NULL)
|
|
851
780
|
AND held_reason IS NOT ?`).run(HELD_REASON_MODAL_CHOICE, timer.id, timer.due_at, timer.body, timer.repeat_every_ms, HELD_REASON_MODAL_CHOICE).changes === 1;
|
|
852
781
|
if (!claimed)
|
|
853
782
|
return false;
|
|
@@ -878,9 +807,8 @@ function noteModalHold(timer, snapshot) {
|
|
|
878
807
|
const claimUnsubmittedInputHoldWithNotice = db.transaction((timer, pane, body) => {
|
|
879
808
|
const claimed = stmt(`UPDATE wakes SET held_at = datetime('now'), held_reason = ?,
|
|
880
809
|
first_held_at = COALESCE(CASE WHEN held_at IS NULL THEN NULL ELSE first_held_at END, datetime('now'))
|
|
881
|
-
WHERE id = ? AND
|
|
810
|
+
WHERE id = ? AND (${activeWakeWhere()})
|
|
882
811
|
AND due_at IS ? AND body IS ? AND repeat_every_ms IS ?
|
|
883
|
-
AND (fired_at IS NULL OR repeat_every_ms IS NOT NULL)
|
|
884
812
|
AND held_reason IS NOT ?`).run(HELD_REASON_UNSUBMITTED_INPUT, timer.id, timer.due_at, timer.body, timer.repeat_every_ms, HELD_REASON_UNSUBMITTED_INPUT).changes === 1;
|
|
885
813
|
if (!claimed)
|
|
886
814
|
return false;
|
|
@@ -942,7 +870,7 @@ function blockedWatchedAgents(timer, snapshot, tellActor) {
|
|
|
942
870
|
blockedSince: r.episode,
|
|
943
871
|
}));
|
|
944
872
|
}
|
|
945
|
-
const stillPending = (timerId) => stmt(`SELECT 1 AS hit FROM wakes WHERE id = ? AND
|
|
873
|
+
const stillPending = (timerId) => stmt(`SELECT 1 AS hit FROM wakes WHERE id = ? AND ${pendingOneShotWhere()}`).get(timerId) !== undefined;
|
|
946
874
|
const claimBlockNoticeWithNotice = db.transaction((timer, agentId, blockedSince, tell, body) => {
|
|
947
875
|
if (!stillPending(timer.id))
|
|
948
876
|
return false;
|
|
@@ -998,9 +926,6 @@ function alreadyToldAbout(timerId, agentId, blockedSince) {
|
|
|
998
926
|
}
|
|
999
927
|
const CONDITION_IDLE = "idle";
|
|
1000
928
|
const CONDITION_GONE = "gone";
|
|
1001
|
-
const NOTICE_RETRY_AFTER = "-60 seconds";
|
|
1002
|
-
const spentUntypedNotice = (alias) => `${alias}.fired_at IS NOT NULL AND COALESCE(${alias}.typed_at, ${alias}.socket_attempt_at) IS NULL ` +
|
|
1003
|
-
`AND ${alias}.cancelled_at IS NULL AND ${alias}.fired_at < datetime('now', '${NOTICE_RETRY_AFTER}')`;
|
|
1004
929
|
const NOTICE_MAX_AGE = "-1 hours";
|
|
1005
930
|
const ROSTER_STILL_GOING = 8;
|
|
1006
931
|
const FINISHED_SHOWN_CAP = ROSTER_STILL_GOING;
|
|
@@ -1023,13 +948,13 @@ function standingIdleRows(timer) {
|
|
|
1023
948
|
AND a.agent_state = 'idle' AND a.state_changed_at IS NOT NULL
|
|
1024
949
|
${OWNED_BY_WATCH}
|
|
1025
950
|
AND ${unreported(CONDITION_IDLE, "a.state_changed_at")}
|
|
1026
|
-
ORDER BY a.id`).all(timer.project_id, timer.deliver_actor, timer.owner, timer.id).filter(
|
|
951
|
+
ORDER BY a.id`).all(timer.project_id, timer.deliver_actor, timer.owner, timer.id).filter(finishedIdle);
|
|
1027
952
|
}
|
|
1028
953
|
function standingGoneRows(timer) {
|
|
1029
954
|
return stmt(`SELECT ${CREW_COLUMNS}, a.closed_at AS episode
|
|
1030
955
|
FROM agents a
|
|
1031
956
|
WHERE a.project_id = ? AND a.kind = 'agent' AND a.status = 'closed' AND a.actor_id != ?
|
|
1032
|
-
AND (
|
|
957
|
+
AND NOT (${finishedIdleSql("a")})
|
|
1033
958
|
AND a.parked_at = ''
|
|
1034
959
|
AND a.closed_at IS NOT NULL
|
|
1035
960
|
${OWNED_BY_WATCH}
|
|
@@ -1042,7 +967,7 @@ export function markGoneReported(agentId, projectId) {
|
|
|
1042
967
|
FROM wakes t
|
|
1043
968
|
JOIN agents a ON a.id = ?
|
|
1044
969
|
WHERE t.project_id = ? AND t.kind = 'idle_any' AND t.watch_scope IS NOT NULL
|
|
1045
|
-
AND t.
|
|
970
|
+
AND ${pendingOneShotWhere("t.")}
|
|
1046
971
|
AND a.closed_at IS NOT NULL`).run(agentId, projectId);
|
|
1047
972
|
}
|
|
1048
973
|
export function seedGoneCursor(timerId, projectId) {
|
|
@@ -1077,7 +1002,7 @@ function stillGoingRows(timer) {
|
|
|
1077
1002
|
return stmt(`SELECT a.name, a.actor_id, a.agent_state, a.state_changed_at, a.status, a.command, a.kind, a.resumed_at
|
|
1078
1003
|
FROM agents a
|
|
1079
1004
|
WHERE a.project_id = ? AND a.kind = 'agent' AND a.status = 'running'
|
|
1080
|
-
AND (
|
|
1005
|
+
AND NOT (${finishedIdleSql("a")}) AND a.actor_id != ?
|
|
1081
1006
|
${OWNED_BY_WATCH}
|
|
1082
1007
|
ORDER BY a.id`).all(timer.project_id, timer.deliver_actor, timer.owner);
|
|
1083
1008
|
}
|
|
@@ -1226,7 +1151,7 @@ function standingNoticeBody(timer, finished, totalFinished, carriedTotal, span)
|
|
|
1226
1151
|
more = Math.max(0, rows.length - ROSTER_STILL_GOING);
|
|
1227
1152
|
shown = rows
|
|
1228
1153
|
.slice(0, ROSTER_STILL_GOING)
|
|
1229
|
-
.map((r) =>
|
|
1154
|
+
.map((r) => r.agent_state === "idle" && !finishedIdle(r)
|
|
1230
1155
|
? `${r.name} (awaiting first assignment)`
|
|
1231
1156
|
: `${r.name} (${stateNowClause(r)})`);
|
|
1232
1157
|
}
|
|
@@ -1511,8 +1436,7 @@ function agedOutBody(timer) {
|
|
|
1511
1436
|
// Telling a lead it need not re-arm, in hive's own generated prose, when it must, is how a lane ends
|
|
1512
1437
|
// up sitting finished and unnoticed - which this project has paid for twice.
|
|
1513
1438
|
export function watchStillWatchingClause(timer) {
|
|
1514
|
-
const parent = stmt(`SELECT (
|
|
1515
|
-
AND (max_wait_at IS NULL OR max_wait_at > datetime('now'))) AS watching,
|
|
1439
|
+
const parent = stmt(`SELECT (${watchWatchingWhere()}) AS watching,
|
|
1516
1440
|
(watch_scope IS NOT NULL) AS standing
|
|
1517
1441
|
FROM wakes WHERE id = ?`).get(timer.parent_wake_id);
|
|
1518
1442
|
// The parent is not always a standing watch: a modal-hold or one-shot block notice's parent is
|
|
@@ -1660,39 +1584,28 @@ function deliverable(timer, snapshot, choices) {
|
|
|
1660
1584
|
owner,
|
|
1661
1585
|
};
|
|
1662
1586
|
}
|
|
1663
|
-
const PANE_CLAIM_TTL_SECONDS = 6;
|
|
1664
|
-
const PANE_CLAIM_SETTLE_MS = 500;
|
|
1665
1587
|
const PANE_CLAIM_OWNER = "hive:scheduler";
|
|
1666
|
-
function acquirePaneClaim(projectId, key) {
|
|
1667
|
-
try {
|
|
1668
|
-
stmt("INSERT OR IGNORE INTO actors (id, name, kind) VALUES (?, 'hive scheduler', 'scheduler')").run(PANE_CLAIM_OWNER);
|
|
1669
|
-
stmt("DELETE FROM leases WHERE project_id = ? AND lock_key = ? AND expires_at < datetime('now')").run(projectId, key);
|
|
1670
|
-
const row = stmt(`INSERT INTO leases (project_id, lock_key, owner, expires_at)
|
|
1671
|
-
VALUES (?, ?, ?, datetime('now', printf('+%d seconds', ?)))
|
|
1672
|
-
ON CONFLICT(project_id, lock_key) DO NOTHING
|
|
1673
|
-
RETURNING expires_at`).get(projectId, key, PANE_CLAIM_OWNER, PANE_CLAIM_TTL_SECONDS);
|
|
1674
|
-
return row?.expires_at ?? null;
|
|
1675
|
-
}
|
|
1676
|
-
catch {
|
|
1677
|
-
return null;
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
1588
|
// Serializes check-then-type per pane ACROSS scheduler instances: the per-wake claim is atomic but two
|
|
1681
1589
|
// instances claiming different wakes for one pane would otherwise both paste before either submits.
|
|
1682
1590
|
// A loser skips this tick and its wake stays unclaimed.
|
|
1683
1591
|
async function withPaneClaim(timer, fn) {
|
|
1684
|
-
const
|
|
1685
|
-
|
|
1592
|
+
const held = timer.deliver_pane
|
|
1593
|
+
? tryAcquirePaneLease(timer.project_id, timer.deliver_socket, timer.deliver_pane, {
|
|
1594
|
+
id: PANE_CLAIM_OWNER,
|
|
1595
|
+
name: "hive scheduler",
|
|
1596
|
+
kind: "scheduler",
|
|
1597
|
+
})
|
|
1598
|
+
: null;
|
|
1686
1599
|
if (held === null)
|
|
1687
1600
|
return;
|
|
1688
1601
|
try {
|
|
1689
1602
|
// Settle: a slow TUI still showing the pasted text would make the next instance hold instead of type.
|
|
1690
1603
|
if (await fn())
|
|
1691
|
-
await sleep(
|
|
1604
|
+
await sleep(PANE_LEASE_SETTLE_MS);
|
|
1692
1605
|
}
|
|
1693
1606
|
finally {
|
|
1694
1607
|
// Match this acquire's expires_at: after a TTL takeover the owner is shared, so owner alone would delete the taker's row.
|
|
1695
|
-
|
|
1608
|
+
releasePaneLease(timer.project_id, timer.deliver_socket, timer.deliver_pane, held);
|
|
1696
1609
|
}
|
|
1697
1610
|
}
|
|
1698
1611
|
async function fireDelay(timer, snapshot, choices) {
|
|
@@ -1745,7 +1658,7 @@ async function fireDelayClaimed(timer, snapshot, choices) {
|
|
|
1745
1658
|
function claimOneShot(timer) {
|
|
1746
1659
|
return (stmt(`UPDATE wakes SET fired_at = datetime('now'), fire_count = fire_count + 1
|
|
1747
1660
|
WHERE id = ? AND due_at IS ? AND body IS ? AND repeat_every_ms IS ?
|
|
1748
|
-
AND
|
|
1661
|
+
AND ${pendingOneShotWhere()}`).run(timer.id, timer.due_at, timer.body, timer.repeat_every_ms).changes === 1);
|
|
1749
1662
|
}
|
|
1750
1663
|
const GONE = { idle: true, gone: true, since: null };
|
|
1751
1664
|
const UNKNOWN = { idle: false, gone: false, since: null };
|
|
@@ -1764,7 +1677,7 @@ function watchedStates(timer, snapshot) {
|
|
|
1764
1677
|
return GONE;
|
|
1765
1678
|
}
|
|
1766
1679
|
return {
|
|
1767
|
-
idle:
|
|
1680
|
+
idle: finishedIdle(agent),
|
|
1768
1681
|
gone: false,
|
|
1769
1682
|
since: agent.state_changed_at,
|
|
1770
1683
|
};
|
|
@@ -2003,24 +1916,8 @@ function recordLostAfterClaim(timer, pasted, why, firing = null) {
|
|
|
2003
1916
|
cancelled_at = CASE WHEN repeat_every_ms IS NULL THEN datetime('now') ELSE cancelled_at END
|
|
2004
1917
|
WHERE id = ? AND (? IS NULL OR fire_count = ?)`, HELD_REASON_OWNERSHIP_LOST_AFTER_CLAIM + why + (pasted ? " The body was pasted; no Enter was sent." : ""), pasted ? 1 : 0, timer.id, firing, firing);
|
|
2005
1918
|
}
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
socketRepeating: "socket-repeating",
|
|
2009
|
-
pty: "pty",
|
|
2010
|
-
ptyAfterSocketTimeout: "pty-after-socket-timeout",
|
|
2011
|
-
};
|
|
2012
|
-
export const socketAwaitingWhere = (prefix = "") => `${prefix}delivery_method = '${DELIVERY_METHOD.socket}' AND ${prefix}confirmed_at IS NULL AND ${prefix}typed_at IS NULL AND ${prefix}cancelled_at IS NULL`;
|
|
2013
|
-
// An attempt older than the row's latest claim belongs to an earlier firing (an old-build claim leaves it behind).
|
|
2014
|
-
const currentAttempt = (p) => `${p}socket_attempt_at IS NOT NULL AND ${p}confirmed_at IS NULL AND ${p}typed_at IS NULL AND ${p}cancelled_at IS NULL
|
|
2015
|
-
AND ${p}fired_at IS NOT NULL AND ${p}socket_attempt_at >= ${p}fired_at`;
|
|
2016
|
-
const repeatingSocketAwaitingWhere = (prefix = "") => `${prefix}delivery_method = '${DELIVERY_METHOD.socketRepeating}' AND ${currentAttempt(prefix)}`;
|
|
2017
|
-
const FIRING_DROPPED_NOTE = "dropped";
|
|
2018
|
-
const repeatingFallbackClaimedWhere = (prefix = "") => `${prefix}repeat_every_ms IS NOT NULL AND ${prefix}delivery_method = '${DELIVERY_METHOD.ptyAfterSocketTimeout}' AND ${currentAttempt(prefix)}
|
|
2019
|
-
AND ${prefix}socket_delivery_note IS NOT '${FIRING_DROPPED_NOTE}'`;
|
|
2020
|
-
// Bounded like retry and confirmation: past retention neither can settle it, so it must stop deferring.
|
|
2021
|
-
const REPEATING_FIRING_PENDING = `((${repeatingSocketAwaitingWhere()}) OR (${repeatingFallbackClaimedWhere()}))
|
|
2022
|
-
AND socket_attempt_at >= datetime('now', '${LOG_RETENTION}')`;
|
|
2023
|
-
const REPEATING_SOCKET_FIRING = `repeat_every_ms IS NOT NULL AND COALESCE(delivery_method, '') IN ('${DELIVERY_METHOD.socketRepeating}', '${DELIVERY_METHOD.ptyAfterSocketTimeout}')`;
|
|
1919
|
+
const REPEATING_FIRING_PENDING = repeatingFiringPendingWhere();
|
|
1920
|
+
const REPEATING_SOCKET_FIRING = repeatingSocketFiringWhere();
|
|
2024
1921
|
function repeatingFiringPending(id) {
|
|
2025
1922
|
try {
|
|
2026
1923
|
return stmt(`SELECT 1 AS hit FROM wakes WHERE id = ? AND (${REPEATING_FIRING_PENDING})`).get(id) !== undefined;
|
|
@@ -2030,34 +1927,19 @@ function repeatingFiringPending(id) {
|
|
|
2030
1927
|
}
|
|
2031
1928
|
}
|
|
2032
1929
|
const firingMarker = (timer) => `[hive wake #${timer.id} firing #${timer.fire_count}`;
|
|
2033
|
-
const SOCKET_CONFIRM_GRACE_SECONDS = 60;
|
|
2034
1930
|
const withReDelivered = (note) => (note ? `${note}, re-delivered` : "re-delivered");
|
|
2035
1931
|
const promptEvidenceSql = (lowerBound) => `
|
|
2036
1932
|
SELECT MIN(created_at) FROM agent_state_log
|
|
2037
1933
|
WHERE actor_id = wakes.deliver_actor AND event = 'prompt' AND created_at >= ${lowerBound}
|
|
2038
1934
|
AND (payload LIKE '%[hive wake #' || wakes.id || ']%'
|
|
2039
1935
|
OR payload LIKE '%[hive wake #' || wakes.id || ',%')`;
|
|
2040
|
-
const ENVELOPE_OPEN = '<cross-session-message from="';
|
|
2041
|
-
const ENVELOPE_SENDER_CLOSE = '" from-name="hive">';
|
|
2042
|
-
const EV_PROMPT = "(CASE WHEN json_valid(ev.payload) THEN COALESCE(json_extract(ev.payload, '$.prompt'), '') ELSE ev.payload END)";
|
|
2043
1936
|
const EV_MARKER = "('[hive wake #' || wakes.id || ' firing #' || wakes.fire_count)";
|
|
2044
|
-
const markerAt = (
|
|
2045
|
-
const EV_HEADER_END = `instr(${EV_PROMPT}, char(10))`;
|
|
1937
|
+
const markerAt = (promptSql, startSql) => `substr(${promptSql}, ${startSql}, length(${EV_MARKER}) + 1) IN (${EV_MARKER} || ']', ${EV_MARKER} || ',')`;
|
|
2046
1938
|
// The current firing's exact marker, at prompt offset 0 (typed) or opening hive's envelope body (socket).
|
|
2047
1939
|
const repeatingEvidenceSql = (select) => `
|
|
2048
1940
|
SELECT ${select} FROM agent_state_log ev
|
|
2049
1941
|
WHERE ev.actor_id = wakes.deliver_actor AND ev.event = 'prompt' AND ev.created_at >= wakes.socket_attempt_at
|
|
2050
|
-
AND
|
|
2051
|
-
OR (${EV_HEADER_END} > ${ENVELOPE_OPEN.length + ENVELOPE_SENDER_CLOSE.length}
|
|
2052
|
-
AND substr(${EV_PROMPT}, 1, ${ENVELOPE_OPEN.length}) = '${ENVELOPE_OPEN}'
|
|
2053
|
-
AND substr(${EV_PROMPT}, ${EV_HEADER_END} - ${ENVELOPE_SENDER_CLOSE.length}, ${ENVELOPE_SENDER_CLOSE.length}) = '${ENVELOPE_SENDER_CLOSE}'
|
|
2054
|
-
AND instr(substr(${EV_PROMPT}, ${ENVELOPE_OPEN.length + 1}, ${EV_HEADER_END} - ${ENVELOPE_OPEN.length + ENVELOPE_SENDER_CLOSE.length + 1}), '"') = 0
|
|
2055
|
-
AND ${markerAt(`${EV_HEADER_END} + 1`)}
|
|
2056
|
-
AND instr(${EV_PROMPT}, char(10) || '${CROSS_SESSION_CLOSE}') > ${EV_HEADER_END}))`;
|
|
2057
|
-
function quietMessagingOn(projectId) {
|
|
2058
|
-
const project = stmt("SELECT path FROM projects WHERE id = ?").get(projectId);
|
|
2059
|
-
return project !== undefined && loadProjectYml(project.path).config?.quiet_messaging === true;
|
|
2060
|
-
}
|
|
1942
|
+
AND ${promptMarkerEvidenceSql(markerAt)}`;
|
|
2061
1943
|
// Only a wake to a live, row-owned Claude lead whose socket was registered by this pane epoch.
|
|
2062
1944
|
function socketRoute(timer, owner) {
|
|
2063
1945
|
return (owner !== null &&
|
|
@@ -2141,7 +2023,7 @@ async function deliver(timer, note, choices, typedSeen, firstHeldAt, owner, fall
|
|
|
2141
2023
|
}
|
|
2142
2024
|
const posted = await postClaudeWake({
|
|
2143
2025
|
socketPath: timer.deliver_messaging_socket,
|
|
2144
|
-
senderAddress:
|
|
2026
|
+
senderAddress: socketSenderAddress(dataDir),
|
|
2145
2027
|
text,
|
|
2146
2028
|
beforeWrite: () => socketStillRegistered(timer, owner),
|
|
2147
2029
|
});
|
|
@@ -2220,15 +2102,7 @@ async function deliver(timer, note, choices, typedSeen, firstHeldAt, owner, fall
|
|
|
2220
2102
|
}
|
|
2221
2103
|
// Measured from the lead's first turn end after the post, because a busy lead reads the post only then.
|
|
2222
2104
|
function socketFallbackDue(timer) {
|
|
2223
|
-
|
|
2224
|
-
let from = firstStop;
|
|
2225
|
-
if (from === null) {
|
|
2226
|
-
const turn = timer.deliver_row_id === null ? null : readLeadTurnState(timer.deliver_row_id);
|
|
2227
|
-
if (turn !== null && turn.state === "working" && turn.pane_pid === timer.deliver_pane_pid)
|
|
2228
|
-
return false;
|
|
2229
|
-
from = timer.socket_attempt_at;
|
|
2230
|
-
}
|
|
2231
|
-
return stmt("SELECT (julianday('now') - julianday(?)) * 86400 >= ? AS due").get(from, SOCKET_CONFIRM_GRACE_SECONDS).due === 1;
|
|
2105
|
+
return socketGraceDue(timer.deliver_actor, timer.socket_attempt_at, timer.deliver_pane_pid, () => timer.deliver_row_id === null ? null : readLeadTurnState(timer.deliver_row_id));
|
|
2232
2106
|
}
|
|
2233
2107
|
async function fallbackClaimed(timer, snapshot, choices) {
|
|
2234
2108
|
const decision = deliverable(timer, snapshot, choices);
|
package/dist/staleServers.js
CHANGED
|
@@ -3,6 +3,8 @@ import { realpathSync } from "node:fs";
|
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { DEFAULT_DATA_DIR } from "./dataDir.js";
|
|
6
|
+
import { parseEtimeSeconds } from "./etime.js";
|
|
7
|
+
import { parseStoreTimestamp } from "./storeClock.js";
|
|
6
8
|
export const STALE_SERVER_BOUNDARY_SLACK_MS = 3000;
|
|
7
9
|
export function readProcessEnvironment(pid) {
|
|
8
10
|
try {
|
|
@@ -18,37 +20,11 @@ export function readProcessEnvironment(pid) {
|
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
const serverEntrypoint = resolve(dirname(fileURLToPath(import.meta.url)), "index.js");
|
|
21
|
-
export
|
|
22
|
-
const dayForm = /^(\d+)-(\d{1,2}):(\d{2}):(\d{2})$/.exec(value);
|
|
23
|
-
const fields = dayForm ? dayForm.slice(2).map(Number) : value.split(":").map(Number);
|
|
24
|
-
if ((!dayForm && fields.length !== 2 && fields.length !== 3) || fields.some((field) => !Number.isSafeInteger(field) || field < 0)) {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
let total;
|
|
28
|
-
if (dayForm) {
|
|
29
|
-
const [hours, minutes, seconds] = fields;
|
|
30
|
-
if (hours > 23 || minutes > 59 || seconds > 59)
|
|
31
|
-
return null;
|
|
32
|
-
total = (Number(dayForm[1]) * 24 + hours) * 3600 + minutes * 60 + seconds;
|
|
33
|
-
}
|
|
34
|
-
else if (fields.length === 2) {
|
|
35
|
-
const [minutes, seconds] = fields;
|
|
36
|
-
if (seconds > 59)
|
|
37
|
-
return null;
|
|
38
|
-
total = minutes * 60 + seconds;
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
const [hours, minutes, seconds] = fields;
|
|
42
|
-
if (minutes > 59 || seconds > 59)
|
|
43
|
-
return null;
|
|
44
|
-
total = hours * 3600 + minutes * 60 + seconds;
|
|
45
|
-
}
|
|
46
|
-
return Number.isSafeInteger(total) ? total : null;
|
|
47
|
-
}
|
|
23
|
+
export const elapsedSeconds = parseEtimeSeconds;
|
|
48
24
|
function appliedAtInstant(appliedAt) {
|
|
49
25
|
if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(appliedAt))
|
|
50
26
|
return null;
|
|
51
|
-
const instant =
|
|
27
|
+
const instant = parseStoreTimestamp(appliedAt);
|
|
52
28
|
return Number.isFinite(instant) ? instant : null;
|
|
53
29
|
}
|
|
54
30
|
function runsThisServer(command) {
|
package/dist/stateProvenance.js
CHANGED
|
@@ -2,6 +2,7 @@ import { db } from "./db.js";
|
|
|
2
2
|
import { awaitingFirstPrompt } from "./firstPrompt.js";
|
|
3
3
|
import { harnessFor } from "./harnesses.js";
|
|
4
4
|
import { sanitizeEventForDisplay } from "./tmux.js";
|
|
5
|
+
import { parseStoreTimestamp } from "./storeClock.js";
|
|
5
6
|
const prepared = new Map();
|
|
6
7
|
function stmt(sql) {
|
|
7
8
|
let s = prepared.get(sql);
|
|
@@ -11,9 +12,6 @@ function stmt(sql) {
|
|
|
11
12
|
}
|
|
12
13
|
return s;
|
|
13
14
|
}
|
|
14
|
-
function parseStoreTimestamp(ts) {
|
|
15
|
-
return new Date(`${ts.replace(" ", "T")}Z`).getTime();
|
|
16
|
-
}
|
|
17
15
|
export function ageSecondsSince(storeTimestamp, now = Date.now()) {
|
|
18
16
|
return Math.max(0, Math.round((now - parseStoreTimestamp(storeTimestamp)) / 1000));
|
|
19
17
|
}
|
package/dist/teardown.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { parseStoreTimestamp } from "./storeClock.js";
|
|
3
4
|
export const TEARDOWN_LOG = "teardowns.jsonl";
|
|
4
5
|
// A backstop against a runaway writer, not a retention policy.
|
|
5
6
|
export const TEARDOWN_MAX_RECORDS = 200;
|
|
@@ -11,7 +12,7 @@ export function teardownLogPath(dataDir) {
|
|
|
11
12
|
export function sqlNow(now = new Date()) {
|
|
12
13
|
return now.toISOString().replace("T", " ").slice(0, 23);
|
|
13
14
|
}
|
|
14
|
-
const sqlToDate = (ts) => new Date(
|
|
15
|
+
const sqlToDate = (ts) => new Date(parseStoreTimestamp(ts));
|
|
15
16
|
export function teardownWindow(from, to, basis) {
|
|
16
17
|
const seconds = (sqlToDate(to).getTime() - sqlToDate(from).getTime()) / 1000;
|
|
17
18
|
return { from, to, basis, width_seconds: Number.isFinite(seconds) ? Math.max(0, seconds) : 0 };
|
package/dist/tools/leases.js
CHANGED
|
@@ -1,21 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { db } from "../db.js";
|
|
3
2
|
import { currentActor, effectiveProjectId } from "../context.js";
|
|
3
|
+
import { extendOwnedLease, insertLease, purgeExpiredLeases, readLease, releaseOwnedLease } from "../leaseStore.js";
|
|
4
4
|
import { run } from "../result.js";
|
|
5
5
|
import { idParam, projectIdParam } from "./params.js";
|
|
6
|
-
function purgeExpired(projectId) {
|
|
7
|
-
db.prepare("DELETE FROM leases WHERE project_id = ? AND expires_at < datetime('now')").run(projectId);
|
|
8
|
-
}
|
|
9
|
-
export function readLease(projectId, key) {
|
|
10
|
-
return db.prepare("SELECT * FROM leases WHERE project_id = ? AND lock_key = ?").get(projectId, key);
|
|
11
|
-
}
|
|
12
|
-
export function extendOwnedLease(projectId, key, actor, ttlSeconds) {
|
|
13
|
-
const info = db
|
|
14
|
-
.prepare(`UPDATE leases SET expires_at = datetime('now', printf('+%d seconds', ?))
|
|
15
|
-
WHERE project_id = ? AND lock_key = ? AND owner = ?`)
|
|
16
|
-
.run(ttlSeconds, projectId, key, actor);
|
|
17
|
-
return info.changes > 0;
|
|
18
|
-
}
|
|
19
6
|
export function registerLeases(server) {
|
|
20
7
|
server.registerTool("lease_acquire", {
|
|
21
8
|
description: "Try to take a named lease on state another session could also change, such as a shared checkout, a dev database or a port (non-blocking). Re-taking your own lease extends it. Leases expire on their own.",
|
|
@@ -40,14 +27,9 @@ export function registerLeases(server) {
|
|
|
40
27
|
},
|
|
41
28
|
}, (args) => run(() => {
|
|
42
29
|
const projectId = effectiveProjectId(args.project_id);
|
|
43
|
-
|
|
30
|
+
purgeExpiredLeases(projectId);
|
|
44
31
|
const actor = currentActor();
|
|
45
|
-
const inserted =
|
|
46
|
-
.prepare(`INSERT INTO leases (project_id, lock_key, owner, expires_at)
|
|
47
|
-
VALUES (?, ?, ?, datetime('now', printf('+%d seconds', ?)))
|
|
48
|
-
ON CONFLICT(project_id, lock_key) DO NOTHING
|
|
49
|
-
RETURNING expires_at`)
|
|
50
|
-
.get(projectId, args.key, actor, args.ttl_seconds);
|
|
32
|
+
const inserted = insertLease(projectId, args.key, actor, args.ttl_seconds);
|
|
51
33
|
if (inserted) {
|
|
52
34
|
return { project_id: projectId, key: args.key, acquired: true, expires_at: inserted.expires_at };
|
|
53
35
|
}
|
|
@@ -91,9 +73,7 @@ export function registerLeases(server) {
|
|
|
91
73
|
outputSchema: { project_id: idParam, key: z.string(), released: z.boolean() },
|
|
92
74
|
}, (args) => run(() => {
|
|
93
75
|
const projectId = effectiveProjectId(args.project_id);
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
.run(projectId, args.key, currentActor());
|
|
97
|
-
return { project_id: projectId, key: args.key, released: info.changes > 0 };
|
|
76
|
+
const released = releaseOwnedLease(projectId, args.key, currentActor());
|
|
77
|
+
return { project_id: projectId, key: args.key, released };
|
|
98
78
|
}));
|
|
99
79
|
}
|
package/dist/tools/wakes.js
CHANGED
|
@@ -6,8 +6,8 @@ import { commitQueenStateWrite } from "../queenAudit.js";
|
|
|
6
6
|
import { currentActor, effectiveProjectId, getProject, isQueenLead, } from "../context.js";
|
|
7
7
|
import { run } from "../result.js";
|
|
8
8
|
import { summaryLiveness } from "../agentRecords.js";
|
|
9
|
-
import {
|
|
10
|
-
import { ACTIVE_TIMER_WHERE } from "../wakeSql.js";
|
|
9
|
+
import { OWNED_BY_WATCH, WATCH_SCOPE_PROJECT, seedGoneCursor, } from "../scheduler.js";
|
|
10
|
+
import { ACTIVE_TIMER_WHERE, LOG_RETENTION, firedOneShotWhere, pendingOneShotWhere, wakeConfirmation, } from "../wakeSql.js";
|
|
11
11
|
import { idParam, projectIdParam } from "./params.js";
|
|
12
12
|
import { cutToUnitBudget } from "../slug.js";
|
|
13
13
|
import { awaitingFirstPrompt } from "../firstPrompt.js";
|
|
@@ -29,7 +29,7 @@ const RECENTLY_FIRED_LIMIT = 10;
|
|
|
29
29
|
function recentlyFiredWakes(projectId) {
|
|
30
30
|
return db
|
|
31
31
|
.prepare(`SELECT * FROM wakes
|
|
32
|
-
WHERE project_id = ? AND
|
|
32
|
+
WHERE project_id = ? AND ${firedOneShotWhere()}
|
|
33
33
|
AND fired_at >= datetime('now', ?)
|
|
34
34
|
ORDER BY fired_at DESC, id DESC LIMIT ${RECENTLY_FIRED_LIMIT}`)
|
|
35
35
|
.all(projectId, LOG_RETENTION);
|
|
@@ -52,15 +52,7 @@ function deliveryState(t, hasChannel) {
|
|
|
52
52
|
held_at: t.held_at,
|
|
53
53
|
held_reason: t.held_reason,
|
|
54
54
|
confirmed_at: t.confirmed_at,
|
|
55
|
-
confirmation: t.typed_at == null && t.socket_attempt_at == null
|
|
56
|
-
? null
|
|
57
|
-
: t.confirmed_at != null
|
|
58
|
-
? "confirmed"
|
|
59
|
-
: !hasChannel(t.deliver_actor)
|
|
60
|
-
? "no_confirmation_channel"
|
|
61
|
-
: t.typed_busy === 1
|
|
62
|
-
? "unconfirmed_busy"
|
|
63
|
-
: "unconfirmed",
|
|
55
|
+
confirmation: wakeConfirmation(t, (t.typed_at == null && t.socket_attempt_at == null) || t.confirmed_at != null || hasChannel(t.deliver_actor)),
|
|
64
56
|
delivery_method: t.delivery_method,
|
|
65
57
|
delivered_by: t.delivered_by,
|
|
66
58
|
socket_attempt_at: t.socket_attempt_at,
|
|
@@ -408,13 +400,13 @@ export function registerWakes(server) {
|
|
|
408
400
|
.prepare(`SELECT id, watch FROM wakes
|
|
409
401
|
WHERE project_id = ? AND owner = ? AND kind = ?
|
|
410
402
|
AND watch_scope IS NULL AND deliver_actor = ? AND deliver_pane = ?
|
|
411
|
-
AND
|
|
403
|
+
AND ${pendingOneShotWhere()}
|
|
412
404
|
AND id NOT IN (SELECT wake_id FROM lead_idle_subscriptions)`)
|
|
413
405
|
.all(projectId, currentActor(), `idle_${mode}`, delivery.actor, delivery.pane)
|
|
414
406
|
.filter((w) => sameSet(w.watch))
|
|
415
407
|
.map((w) => w.id);
|
|
416
408
|
const superseded = candidates.filter((id) => db
|
|
417
|
-
.prepare(
|
|
409
|
+
.prepare(`UPDATE wakes SET cancelled_at = datetime('now') WHERE id = ? AND ${pendingOneShotWhere()}`)
|
|
418
410
|
.run(id).changes === 1);
|
|
419
411
|
const info = db
|
|
420
412
|
.prepare(`INSERT INTO wakes (project_id, owner, body, kind, watch, deliver_actor, deliver_pane,
|
|
@@ -577,7 +569,7 @@ export function registerWakes(server) {
|
|
|
577
569
|
const notices = info.changes > 0
|
|
578
570
|
? db
|
|
579
571
|
.prepare(`UPDATE wakes SET cancelled_at = datetime('now')
|
|
580
|
-
WHERE parent_wake_id = ? AND
|
|
572
|
+
WHERE parent_wake_id = ? AND ${pendingOneShotWhere()}`)
|
|
581
573
|
.run(args.wake_id).changes
|
|
582
574
|
: 0;
|
|
583
575
|
return { wake_id: args.wake_id, cancelled: info.changes > 0, cancelled_notices: notices };
|