@cmgmyr/hive 1.11.5 → 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/agentKinds.js +6 -0
- package/dist/agentRecords.js +98 -0
- package/dist/backup.js +1 -1
- package/dist/build-info.json +1 -1
- package/dist/claudeWake.js +52 -4
- package/dist/cli.js +69 -43
- package/dist/context.js +2 -2
- package/dist/crew.js +5 -4
- package/dist/dashboard.js +10 -12
- package/dist/db.js +1 -1
- package/dist/errors.js +3 -0
- package/dist/etime.js +37 -0
- package/dist/firstPrompt.js +6 -0
- package/dist/globalConfig.js +1 -1
- package/dist/heldLabel.js +1 -1
- package/dist/kickoff.js +2 -2
- package/dist/leadHandoff.js +12 -11
- package/dist/leadHandoffCli.js +22 -1
- package/dist/leadHandoffState.js +1 -2
- package/dist/leadLaunch.js +1 -1
- package/dist/leadMessage.js +13 -0
- package/dist/leadMessageDelivery.js +14 -74
- package/dist/leadState.js +3 -2
- package/dist/leadWatch.js +1 -1
- package/dist/leaseStore.js +54 -0
- package/dist/pads.js +70 -0
- package/dist/portfolio.js +7 -3
- package/dist/projectRemove.js +1 -1
- package/dist/projectYml.js +1 -1
- package/dist/ptys.js +2 -9
- package/dist/queenAudit.js +2 -18
- package/dist/queenAuditStore.js +20 -0
- package/dist/queenDashboard.js +2 -1
- package/dist/questions.js +1 -1
- package/dist/result.js +2 -3
- package/dist/scheduler.js +42 -166
- package/dist/spawn.js +2 -6
- 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/todoScope.js +1 -0
- package/dist/todos.js +90 -0
- package/dist/tools/agents.js +4 -96
- package/dist/tools/leases.js +5 -25
- package/dist/tools/pads.js +3 -82
- package/dist/tools/todos.js +6 -89
- package/dist/tools/wakes.js +12 -88
- package/dist/wakeSql.js +135 -0
- package/dist/wakeTarget.js +76 -0
- package/package.json +1 -1
package/dist/queenDashboard.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join, sep } from "node:path";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { db } from "./db.js";
|
|
5
5
|
import { SECTION_STYLE, SPRITE, chevron, renderBoardPadsTodos, sectionScript } from "./dashboard.js";
|
|
6
|
+
import { parseStoreTimestamp } from "./storeClock.js";
|
|
6
7
|
export const QUEEN_BRIEF_KEY = "queen:brief";
|
|
7
8
|
const LANE_ORDER = ["waiting_on_you", "stuck", "moving", "quiet"];
|
|
8
9
|
const LANE_RANK = { waiting_on_you: 0, stuck: 1, moving: 2, quiet: 3 };
|
|
@@ -103,7 +104,7 @@ const REASON_SEVERE = new Set([
|
|
|
103
104
|
"wake_overdue_5m",
|
|
104
105
|
]);
|
|
105
106
|
function parseUtc(ts) {
|
|
106
|
-
return
|
|
107
|
+
return parseStoreTimestamp(ts);
|
|
107
108
|
}
|
|
108
109
|
function age(from, asOf) {
|
|
109
110
|
const seconds = Math.max(0, Math.floor((parseUtc(asOf) - parseUtc(from)) / 1000));
|
package/dist/questions.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { db } from "./db.js";
|
|
2
2
|
import { HUMAN_ACTOR_PREFIX } from "./context.js";
|
|
3
|
-
import { assertSafeWakeBody, resolveWakeDelivery } from "./
|
|
3
|
+
import { assertSafeWakeBody, resolveWakeDelivery } from "./wakeTarget.js";
|
|
4
4
|
const QUESTION_STATE_CONFLICT = "QUESTION_STATE_CONFLICT";
|
|
5
5
|
const HEADER_MAX_CHARS = 12;
|
|
6
6
|
const MIN_OPTIONS = 2;
|
package/dist/result.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { runningBuildChange, runningBuildNotice } from "./version.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
2
|
+
import { errorMessage } from "./errors.js";
|
|
3
|
+
export { errorMessage } from "./errors.js";
|
|
5
4
|
function ok(data) {
|
|
6
5
|
const text = typeof data === "string" ? data : JSON.stringify(data, null, 1);
|
|
7
6
|
return { content: [{ type: "text", text }] };
|
package/dist/scheduler.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { QUEEN_AUDIT_RETENTION, QUEEN_AUDIT_MAX_ROWS, recentQueenAudit } from "./queenAudit.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";
|
|
2
5
|
import { runningBuildChange, runningBuildNotice, versionInfo } from "./version.js";
|
|
3
6
|
import { existsSync, lstatSync, mkdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
7
|
import { createHash } from "node:crypto";
|
|
@@ -13,14 +16,16 @@ import { getProjectByPath, listProjects, queenHomeDir } from "./context.js";
|
|
|
13
16
|
import { collectPortfolio } from "./portfolio.js";
|
|
14
17
|
import { QUEEN_GENERATED_MARKER, readQueenBrief, renderQueenDashboard } from "./queenDashboard.js";
|
|
15
18
|
import { COMMAND_KIND, stoppingMarkerLive } from "./processes.js";
|
|
16
|
-
import { closeAgentRow,
|
|
17
|
-
import {
|
|
19
|
+
import { closeAgentRow, PaneOwnershipLost, reapCodexHomeForClosedAgent, requireStillOwned, } from "./spawn.js";
|
|
20
|
+
import { isLeadActorId, LEAD_ACTOR_PREFIX, LEAD_KIND, } from "./agentKinds.js";
|
|
21
|
+
import { awaitingFirstPrompt, finishedIdle, finishedIdleSql } from "./firstPrompt.js";
|
|
18
22
|
import { evaluateLeadWatch, leadSubscription } from "./leadWatch.js";
|
|
19
23
|
import { describeLiveTasks, describeOneTask, liveBackgroundTasks, STALL_BOUND_SECONDS, } from "./backgroundTasks.js";
|
|
20
24
|
import { MESSAGE_MAX_ROWS, MESSAGE_RETENTION } from "./leadMessage.js";
|
|
21
|
-
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";
|
|
22
27
|
import { driveLeadHandoff } from "./leadHandoff.js";
|
|
23
|
-
import { handoffHoldsWake
|
|
28
|
+
import { handoffHoldsWake } from "./leadHandoffState.js";
|
|
24
29
|
import { FIRST_MESSAGE_LOG_STATE, humanPromptSql, readLeadTurnState } from "./leadState.js";
|
|
25
30
|
import { retryQuietLeadMessages } from "./leadMessageDelivery.js";
|
|
26
31
|
import { harnessFor, hasTranscriptSignal, paneClassifierFor, screenClassifiable, transcriptDirFor } from "./harnesses.js";
|
|
@@ -56,7 +61,6 @@ function deliveryOwnership(row, snapshot) {
|
|
|
56
61
|
return "closed";
|
|
57
62
|
return rowOwnership(deliveryIdentity(row), snapshot);
|
|
58
63
|
}
|
|
59
|
-
export const ACTIVE_TIMER_WHERE = "cancelled_at IS NULL AND (fired_at IS NULL OR repeat_every_ms IS NOT NULL)";
|
|
60
64
|
const SETTLE_WINDOW = "-15 seconds";
|
|
61
65
|
const prepared = new Map();
|
|
62
66
|
function stmt(sql) {
|
|
@@ -280,7 +284,7 @@ function cancelTimer(timerId) {
|
|
|
280
284
|
function cancelWithReason(timerId, reason) {
|
|
281
285
|
bestEffortRun("UPDATE wakes SET cancelled_at = datetime('now'), held_reason = ? WHERE id = ? AND cancelled_at IS NULL", reason, timerId);
|
|
282
286
|
}
|
|
283
|
-
export
|
|
287
|
+
export { LOG_RETENTION } from "./wakeSql.js";
|
|
284
288
|
const LOG_MAX_ROWS = 20_000;
|
|
285
289
|
function pruneStateLog() {
|
|
286
290
|
try {
|
|
@@ -468,7 +472,7 @@ function maybeGenerateDashboards() {
|
|
|
468
472
|
}
|
|
469
473
|
}
|
|
470
474
|
export function checkConfirmations() {
|
|
471
|
-
const sentAt = "
|
|
475
|
+
const sentAt = sentWakeAtSql("wakes.");
|
|
472
476
|
const confirmationQuery = promptEvidenceSql(sentAt);
|
|
473
477
|
try {
|
|
474
478
|
const pending = stmt(`SELECT 1 AS hit FROM wakes
|
|
@@ -540,11 +544,7 @@ export async function tick(snapshot) {
|
|
|
540
544
|
const now = stmt("SELECT datetime('now') AS now").get().now;
|
|
541
545
|
const candidates = stmt(`SELECT wakes.*, ${DELIVER_ROW_COLUMNS}
|
|
542
546
|
FROM wakes ${DELIVER_SOCKET_JOIN}
|
|
543
|
-
WHERE wakes.
|
|
544
|
-
(wakes.kind = 'delay' AND wakes.due_at <= datetime('now')
|
|
545
|
-
AND (wakes.fired_at IS NULL OR wakes.repeat_every_ms IS NOT NULL))
|
|
546
|
-
OR (wakes.kind != 'delay' AND wakes.fired_at IS NULL)
|
|
547
|
-
)`).all();
|
|
547
|
+
WHERE ${schedulerCandidateWhere("wakes.")}`).all();
|
|
548
548
|
const choices = new Map();
|
|
549
549
|
for (const timer of candidates) {
|
|
550
550
|
await isolated(`timer ${timer.id}`, async () => {
|
|
@@ -620,82 +620,14 @@ function holdTimer(timer, reason) {
|
|
|
620
620
|
const count = timer.fire_count ?? null;
|
|
621
621
|
bestEffortRun(`UPDATE wakes SET held_at = datetime('now'), held_reason = ?,
|
|
622
622
|
first_held_at = COALESCE(CASE WHEN held_at IS NULL THEN NULL ELSE first_held_at END, datetime('now'))
|
|
623
|
-
WHERE id = ? AND
|
|
624
|
-
AND (fired_at IS NULL OR (repeat_every_ms IS NOT NULL AND due_at = ? AND (? IS NULL OR fire_count = ?))
|
|
625
|
-
OR (${socketAwaitingWhere()}))`, reason, timer.id, timer.due_at, count, count);
|
|
623
|
+
WHERE id = ? AND ${holdableWakeWhere()}`, reason, timer.id, timer.due_at, count, count);
|
|
626
624
|
}
|
|
627
|
-
const HELD_REASON_MODAL_CHOICE = "pane is awaiting a modal choice (folder-trust or /model picker)";
|
|
628
625
|
const LEAD_BOOT_BOX_WAIT_MS = 60_000;
|
|
629
|
-
const HELD_REASON_LEAD_BOOT_INPUT_BOX = "the lead's input box is not on screen yet; holding while its session starts";
|
|
630
|
-
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 " +
|
|
631
|
-
"cancellation for this alone, so it is held rather than lost";
|
|
632
|
-
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 " +
|
|
633
|
-
"against (its pid no longer matches, most likely a tmux server restart reissuing the id); held " +
|
|
634
|
-
"rather than typed into the wrong pane - ";
|
|
635
|
-
const HELD_REASON_PANE_REISSUED_LEAD = `${HELD_REASON_PANE_REISSUED_PREFIX}run \`hive lead\` to re-point it at the live one`;
|
|
636
|
-
const HELD_REASON_PANE_REISSUED_WORKER = `${HELD_REASON_PANE_REISSUED_PREFIX}nothing re-points a worker's wake automatically, so cancel ` +
|
|
637
|
-
"it with wake_cancel and set a fresh one once the worker's pane is confirmed live, or leave it: " +
|
|
638
|
-
"it will keep holding rather than deliver wrongly";
|
|
639
|
-
const HELD_REASON_PANE_REISSUED_THEN_DEAD = `${HELD_REASON_PANE_REISSUED_PREFIX}the pane it was reissued to has since gone dead too; nothing ` +
|
|
640
|
-
"re-points a worker's wake automatically, so cancel it with wake_cancel (any running lead may do " +
|
|
641
|
-
"this even though the wake is not theirs) if it is no longer needed";
|
|
642
|
-
export function wasHeldForPaneReissue(heldReason) {
|
|
643
|
-
return heldReason != null && heldReason.startsWith(HELD_REASON_PANE_REISSUED_PREFIX);
|
|
644
|
-
}
|
|
645
|
-
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 " +
|
|
646
|
-
"records a different pane, or the pane is on another tmux socket); held rather than typed into a pane " +
|
|
647
|
-
"that may not be its own - ";
|
|
648
|
-
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`;
|
|
649
|
-
const HELD_REASON_PANE_UNKNOWN_WORKER = `${HELD_REASON_PANE_UNKNOWN_PREFIX}nothing re-points a worker's wake automatically, so cancel it with ` +
|
|
650
|
-
"wake_cancel (any running lead may, even though the wake is not theirs), or leave it: it keeps holding " +
|
|
651
|
-
"rather than deliver wrongly";
|
|
652
|
-
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 ` +
|
|
653
|
-
"automatically, so cancel it with wake_cancel (any running lead may) if it is no longer needed";
|
|
654
|
-
// One family for both identity holds, so a later dead pane re-holds them instead of cancelling.
|
|
655
|
-
export function wasHeldForPaneIdentity(heldReason) {
|
|
656
|
-
return wasHeldForPaneReissue(heldReason) || (heldReason?.startsWith(HELD_REASON_PANE_UNKNOWN_PREFIX) ?? false);
|
|
657
|
-
}
|
|
658
|
-
const identityThenDead = (heldReason) => wasHeldForPaneReissue(heldReason) ? HELD_REASON_PANE_REISSUED_THEN_DEAD : HELD_REASON_PANE_UNKNOWN_THEN_DEAD;
|
|
659
|
-
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 " +
|
|
660
|
-
"cancelled rather than delivered into a pane that row no longer owns";
|
|
661
|
-
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 " +
|
|
662
|
-
"lead and re-points it";
|
|
663
|
-
export function isLeadRowClosedHold(heldReason) {
|
|
664
|
-
return heldReason === HELD_REASON_LEAD_ROW_CLOSED;
|
|
665
|
-
}
|
|
666
|
-
const HELD_REASON_OWNERSHIP_LOST_AFTER_CLAIM = "delivery stopped after claim: ";
|
|
667
|
-
export const HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX = "the pane's harness is not one hive can classify; ";
|
|
668
|
-
export const HELD_REASON_UNCLASSIFIABLE_PANE = `${HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX}neither the dialog guard nor the unsubmitted-text ` +
|
|
669
|
-
"guard can read this screen and delivery would type blind. wake_set and wake_when_idle refuse such a " +
|
|
670
|
-
"target outright now, so this wake predates that or its pane's command changed underneath it. Nothing " +
|
|
671
|
-
"hive does on its own clears this: it lifts only when that pane is replaced by one hive starts itself " +
|
|
672
|
-
"(`hive lead` after the pane exits). Cancel it with wake_cancel (any running lead may, even though the " +
|
|
673
|
-
"wake is not theirs) and drive that pane with agent_send(keys: [...])";
|
|
674
|
-
export function isUnclassifiablePaneHold(heldReason) {
|
|
675
|
-
return heldReason != null && heldReason.startsWith(HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX);
|
|
676
|
-
}
|
|
677
|
-
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 " +
|
|
678
|
-
"with no paste markers (losing everything before the last 1022-byte write) and the Enter would be " +
|
|
679
|
-
"eaten by the mode instead of submitting - both silently, with tmux reporting success. It delivers " +
|
|
680
|
-
"once whoever is reading that pane leaves copy mode";
|
|
681
|
-
export const HELD_REASON_UNSUBMITTED_INPUT_PREFIX = "the pane's input box has unsubmitted human text; ";
|
|
682
|
-
export const HELD_REASON_UNSUBMITTED_INPUT = `${HELD_REASON_UNSUBMITTED_INPUT_PREFIX}delivering now would paste the wake body onto it and submit ` +
|
|
683
|
-
"both as one message";
|
|
684
|
-
// Distinct from HELD_REASON_UNSUBMITTED_INPUT so the claim's debounce doesn't latch a transient
|
|
685
|
-
// pane-resolution failure shut forever - the next tick retries instead of giving up permanently.
|
|
686
|
-
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 ` +
|
|
687
|
-
"this tick - retrying, since that may be transient rather than permanent";
|
|
688
|
-
export function isUnsubmittedInputHold(heldReason) {
|
|
689
|
-
return heldReason != null && heldReason.startsWith(HELD_REASON_UNSUBMITTED_INPUT_PREFIX);
|
|
690
|
-
}
|
|
691
|
-
export const HELD_REASON_CONVERSATION = "a human talked to this lead more recently than the conversation-hold window; holding so a wake " +
|
|
692
|
-
"does not split an in-progress discussion - it delivers once the window passes or the hold's own " +
|
|
693
|
-
"ceiling is reached, whichever comes first";
|
|
694
626
|
function leadBootSessionIsFresh(agentId, panePid) {
|
|
695
627
|
const row = stmt("SELECT pane_started_at FROM agents WHERE id = ? AND pane_pid = ?").get(agentId, panePid);
|
|
696
628
|
if (!row?.pane_started_at)
|
|
697
629
|
return false;
|
|
698
|
-
const timestamp =
|
|
630
|
+
const timestamp = parseStoreTimestamp(row.pane_started_at);
|
|
699
631
|
return Number.isFinite(timestamp) && Date.now() - timestamp < LEAD_BOOT_BOX_WAIT_MS;
|
|
700
632
|
}
|
|
701
633
|
function leadInputBoxObservation(pane, command, cache) {
|
|
@@ -826,7 +758,7 @@ function insertNotice(timer, deliverActor, pane, body, parentTimerId) {
|
|
|
826
758
|
function pendingNoticeFor(parentTimerId, conditions) {
|
|
827
759
|
const placeholders = conditions.map(() => "?").join(", ");
|
|
828
760
|
return stmt(`SELECT t.id FROM wakes t
|
|
829
|
-
WHERE t.parent_wake_id = ? AND t.
|
|
761
|
+
WHERE t.parent_wake_id = ? AND ${pendingOneShotWhere("t.")}
|
|
830
762
|
AND EXISTS (
|
|
831
763
|
SELECT 1 FROM wake_idle_notices n WHERE n.notice_wake_id = t.id AND n.condition IN (${placeholders})
|
|
832
764
|
)
|
|
@@ -834,7 +766,7 @@ function pendingNoticeFor(parentTimerId, conditions) {
|
|
|
834
766
|
}
|
|
835
767
|
function updateNoticeInPlace(noticeId, body) {
|
|
836
768
|
return (stmt(`UPDATE wakes SET body = ?, created_at = datetime('now')
|
|
837
|
-
WHERE id = ? AND
|
|
769
|
+
WHERE id = ? AND ${pendingOneShotWhere()}`).run(body, noticeId).changes === 1);
|
|
838
770
|
}
|
|
839
771
|
function claimBlockNotice(timerId, agentId, blockedSince) {
|
|
840
772
|
return (stmt(`INSERT OR IGNORE INTO wake_block_notices (wake_id, agent_id, blocked_since)
|
|
@@ -843,9 +775,8 @@ function claimBlockNotice(timerId, agentId, blockedSince) {
|
|
|
843
775
|
const claimModalHoldWithNotice = db.transaction((timer, pane, body, target) => {
|
|
844
776
|
const claimed = stmt(`UPDATE wakes SET held_at = datetime('now'), held_reason = ?,
|
|
845
777
|
first_held_at = COALESCE(CASE WHEN held_at IS NULL THEN NULL ELSE first_held_at END, datetime('now'))
|
|
846
|
-
WHERE id = ? AND
|
|
778
|
+
WHERE id = ? AND (${activeWakeWhere()})
|
|
847
779
|
AND due_at IS ? AND body IS ? AND repeat_every_ms IS ?
|
|
848
|
-
AND (fired_at IS NULL OR repeat_every_ms IS NOT NULL)
|
|
849
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;
|
|
850
781
|
if (!claimed)
|
|
851
782
|
return false;
|
|
@@ -876,9 +807,8 @@ function noteModalHold(timer, snapshot) {
|
|
|
876
807
|
const claimUnsubmittedInputHoldWithNotice = db.transaction((timer, pane, body) => {
|
|
877
808
|
const claimed = stmt(`UPDATE wakes SET held_at = datetime('now'), held_reason = ?,
|
|
878
809
|
first_held_at = COALESCE(CASE WHEN held_at IS NULL THEN NULL ELSE first_held_at END, datetime('now'))
|
|
879
|
-
WHERE id = ? AND
|
|
810
|
+
WHERE id = ? AND (${activeWakeWhere()})
|
|
880
811
|
AND due_at IS ? AND body IS ? AND repeat_every_ms IS ?
|
|
881
|
-
AND (fired_at IS NULL OR repeat_every_ms IS NOT NULL)
|
|
882
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;
|
|
883
813
|
if (!claimed)
|
|
884
814
|
return false;
|
|
@@ -940,7 +870,7 @@ function blockedWatchedAgents(timer, snapshot, tellActor) {
|
|
|
940
870
|
blockedSince: r.episode,
|
|
941
871
|
}));
|
|
942
872
|
}
|
|
943
|
-
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;
|
|
944
874
|
const claimBlockNoticeWithNotice = db.transaction((timer, agentId, blockedSince, tell, body) => {
|
|
945
875
|
if (!stillPending(timer.id))
|
|
946
876
|
return false;
|
|
@@ -996,9 +926,6 @@ function alreadyToldAbout(timerId, agentId, blockedSince) {
|
|
|
996
926
|
}
|
|
997
927
|
const CONDITION_IDLE = "idle";
|
|
998
928
|
const CONDITION_GONE = "gone";
|
|
999
|
-
const NOTICE_RETRY_AFTER = "-60 seconds";
|
|
1000
|
-
const spentUntypedNotice = (alias) => `${alias}.fired_at IS NOT NULL AND COALESCE(${alias}.typed_at, ${alias}.socket_attempt_at) IS NULL ` +
|
|
1001
|
-
`AND ${alias}.cancelled_at IS NULL AND ${alias}.fired_at < datetime('now', '${NOTICE_RETRY_AFTER}')`;
|
|
1002
929
|
const NOTICE_MAX_AGE = "-1 hours";
|
|
1003
930
|
const ROSTER_STILL_GOING = 8;
|
|
1004
931
|
const FINISHED_SHOWN_CAP = ROSTER_STILL_GOING;
|
|
@@ -1021,13 +948,13 @@ function standingIdleRows(timer) {
|
|
|
1021
948
|
AND a.agent_state = 'idle' AND a.state_changed_at IS NOT NULL
|
|
1022
949
|
${OWNED_BY_WATCH}
|
|
1023
950
|
AND ${unreported(CONDITION_IDLE, "a.state_changed_at")}
|
|
1024
|
-
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);
|
|
1025
952
|
}
|
|
1026
953
|
function standingGoneRows(timer) {
|
|
1027
954
|
return stmt(`SELECT ${CREW_COLUMNS}, a.closed_at AS episode
|
|
1028
955
|
FROM agents a
|
|
1029
956
|
WHERE a.project_id = ? AND a.kind = 'agent' AND a.status = 'closed' AND a.actor_id != ?
|
|
1030
|
-
AND (
|
|
957
|
+
AND NOT (${finishedIdleSql("a")})
|
|
1031
958
|
AND a.parked_at = ''
|
|
1032
959
|
AND a.closed_at IS NOT NULL
|
|
1033
960
|
${OWNED_BY_WATCH}
|
|
@@ -1040,7 +967,7 @@ export function markGoneReported(agentId, projectId) {
|
|
|
1040
967
|
FROM wakes t
|
|
1041
968
|
JOIN agents a ON a.id = ?
|
|
1042
969
|
WHERE t.project_id = ? AND t.kind = 'idle_any' AND t.watch_scope IS NOT NULL
|
|
1043
|
-
AND t.
|
|
970
|
+
AND ${pendingOneShotWhere("t.")}
|
|
1044
971
|
AND a.closed_at IS NOT NULL`).run(agentId, projectId);
|
|
1045
972
|
}
|
|
1046
973
|
export function seedGoneCursor(timerId, projectId) {
|
|
@@ -1075,7 +1002,7 @@ function stillGoingRows(timer) {
|
|
|
1075
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
|
|
1076
1003
|
FROM agents a
|
|
1077
1004
|
WHERE a.project_id = ? AND a.kind = 'agent' AND a.status = 'running'
|
|
1078
|
-
AND (
|
|
1005
|
+
AND NOT (${finishedIdleSql("a")}) AND a.actor_id != ?
|
|
1079
1006
|
${OWNED_BY_WATCH}
|
|
1080
1007
|
ORDER BY a.id`).all(timer.project_id, timer.deliver_actor, timer.owner);
|
|
1081
1008
|
}
|
|
@@ -1224,7 +1151,7 @@ function standingNoticeBody(timer, finished, totalFinished, carriedTotal, span)
|
|
|
1224
1151
|
more = Math.max(0, rows.length - ROSTER_STILL_GOING);
|
|
1225
1152
|
shown = rows
|
|
1226
1153
|
.slice(0, ROSTER_STILL_GOING)
|
|
1227
|
-
.map((r) =>
|
|
1154
|
+
.map((r) => r.agent_state === "idle" && !finishedIdle(r)
|
|
1228
1155
|
? `${r.name} (awaiting first assignment)`
|
|
1229
1156
|
: `${r.name} (${stateNowClause(r)})`);
|
|
1230
1157
|
}
|
|
@@ -1509,8 +1436,7 @@ function agedOutBody(timer) {
|
|
|
1509
1436
|
// Telling a lead it need not re-arm, in hive's own generated prose, when it must, is how a lane ends
|
|
1510
1437
|
// up sitting finished and unnoticed - which this project has paid for twice.
|
|
1511
1438
|
export function watchStillWatchingClause(timer) {
|
|
1512
|
-
const parent = stmt(`SELECT (
|
|
1513
|
-
AND (max_wait_at IS NULL OR max_wait_at > datetime('now'))) AS watching,
|
|
1439
|
+
const parent = stmt(`SELECT (${watchWatchingWhere()}) AS watching,
|
|
1514
1440
|
(watch_scope IS NOT NULL) AS standing
|
|
1515
1441
|
FROM wakes WHERE id = ?`).get(timer.parent_wake_id);
|
|
1516
1442
|
// The parent is not always a standing watch: a modal-hold or one-shot block notice's parent is
|
|
@@ -1658,39 +1584,28 @@ function deliverable(timer, snapshot, choices) {
|
|
|
1658
1584
|
owner,
|
|
1659
1585
|
};
|
|
1660
1586
|
}
|
|
1661
|
-
const PANE_CLAIM_TTL_SECONDS = 6;
|
|
1662
|
-
const PANE_CLAIM_SETTLE_MS = 500;
|
|
1663
1587
|
const PANE_CLAIM_OWNER = "hive:scheduler";
|
|
1664
|
-
function acquirePaneClaim(projectId, key) {
|
|
1665
|
-
try {
|
|
1666
|
-
stmt("INSERT OR IGNORE INTO actors (id, name, kind) VALUES (?, 'hive scheduler', 'scheduler')").run(PANE_CLAIM_OWNER);
|
|
1667
|
-
stmt("DELETE FROM leases WHERE project_id = ? AND lock_key = ? AND expires_at < datetime('now')").run(projectId, key);
|
|
1668
|
-
const row = stmt(`INSERT INTO leases (project_id, lock_key, owner, expires_at)
|
|
1669
|
-
VALUES (?, ?, ?, datetime('now', printf('+%d seconds', ?)))
|
|
1670
|
-
ON CONFLICT(project_id, lock_key) DO NOTHING
|
|
1671
|
-
RETURNING expires_at`).get(projectId, key, PANE_CLAIM_OWNER, PANE_CLAIM_TTL_SECONDS);
|
|
1672
|
-
return row?.expires_at ?? null;
|
|
1673
|
-
}
|
|
1674
|
-
catch {
|
|
1675
|
-
return null;
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
1588
|
// Serializes check-then-type per pane ACROSS scheduler instances: the per-wake claim is atomic but two
|
|
1679
1589
|
// instances claiming different wakes for one pane would otherwise both paste before either submits.
|
|
1680
1590
|
// A loser skips this tick and its wake stays unclaimed.
|
|
1681
1591
|
async function withPaneClaim(timer, fn) {
|
|
1682
|
-
const
|
|
1683
|
-
|
|
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;
|
|
1684
1599
|
if (held === null)
|
|
1685
1600
|
return;
|
|
1686
1601
|
try {
|
|
1687
1602
|
// Settle: a slow TUI still showing the pasted text would make the next instance hold instead of type.
|
|
1688
1603
|
if (await fn())
|
|
1689
|
-
await sleep(
|
|
1604
|
+
await sleep(PANE_LEASE_SETTLE_MS);
|
|
1690
1605
|
}
|
|
1691
1606
|
finally {
|
|
1692
1607
|
// Match this acquire's expires_at: after a TTL takeover the owner is shared, so owner alone would delete the taker's row.
|
|
1693
|
-
|
|
1608
|
+
releasePaneLease(timer.project_id, timer.deliver_socket, timer.deliver_pane, held);
|
|
1694
1609
|
}
|
|
1695
1610
|
}
|
|
1696
1611
|
async function fireDelay(timer, snapshot, choices) {
|
|
@@ -1743,7 +1658,7 @@ async function fireDelayClaimed(timer, snapshot, choices) {
|
|
|
1743
1658
|
function claimOneShot(timer) {
|
|
1744
1659
|
return (stmt(`UPDATE wakes SET fired_at = datetime('now'), fire_count = fire_count + 1
|
|
1745
1660
|
WHERE id = ? AND due_at IS ? AND body IS ? AND repeat_every_ms IS ?
|
|
1746
|
-
AND
|
|
1661
|
+
AND ${pendingOneShotWhere()}`).run(timer.id, timer.due_at, timer.body, timer.repeat_every_ms).changes === 1);
|
|
1747
1662
|
}
|
|
1748
1663
|
const GONE = { idle: true, gone: true, since: null };
|
|
1749
1664
|
const UNKNOWN = { idle: false, gone: false, since: null };
|
|
@@ -1762,7 +1677,7 @@ function watchedStates(timer, snapshot) {
|
|
|
1762
1677
|
return GONE;
|
|
1763
1678
|
}
|
|
1764
1679
|
return {
|
|
1765
|
-
idle:
|
|
1680
|
+
idle: finishedIdle(agent),
|
|
1766
1681
|
gone: false,
|
|
1767
1682
|
since: agent.state_changed_at,
|
|
1768
1683
|
};
|
|
@@ -2001,24 +1916,8 @@ function recordLostAfterClaim(timer, pasted, why, firing = null) {
|
|
|
2001
1916
|
cancelled_at = CASE WHEN repeat_every_ms IS NULL THEN datetime('now') ELSE cancelled_at END
|
|
2002
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);
|
|
2003
1918
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
socketRepeating: "socket-repeating",
|
|
2007
|
-
pty: "pty",
|
|
2008
|
-
ptyAfterSocketTimeout: "pty-after-socket-timeout",
|
|
2009
|
-
};
|
|
2010
|
-
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`;
|
|
2011
|
-
// An attempt older than the row's latest claim belongs to an earlier firing (an old-build claim leaves it behind).
|
|
2012
|
-
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
|
|
2013
|
-
AND ${p}fired_at IS NOT NULL AND ${p}socket_attempt_at >= ${p}fired_at`;
|
|
2014
|
-
const repeatingSocketAwaitingWhere = (prefix = "") => `${prefix}delivery_method = '${DELIVERY_METHOD.socketRepeating}' AND ${currentAttempt(prefix)}`;
|
|
2015
|
-
const FIRING_DROPPED_NOTE = "dropped";
|
|
2016
|
-
const repeatingFallbackClaimedWhere = (prefix = "") => `${prefix}repeat_every_ms IS NOT NULL AND ${prefix}delivery_method = '${DELIVERY_METHOD.ptyAfterSocketTimeout}' AND ${currentAttempt(prefix)}
|
|
2017
|
-
AND ${prefix}socket_delivery_note IS NOT '${FIRING_DROPPED_NOTE}'`;
|
|
2018
|
-
// Bounded like retry and confirmation: past retention neither can settle it, so it must stop deferring.
|
|
2019
|
-
const REPEATING_FIRING_PENDING = `((${repeatingSocketAwaitingWhere()}) OR (${repeatingFallbackClaimedWhere()}))
|
|
2020
|
-
AND socket_attempt_at >= datetime('now', '${LOG_RETENTION}')`;
|
|
2021
|
-
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();
|
|
2022
1921
|
function repeatingFiringPending(id) {
|
|
2023
1922
|
try {
|
|
2024
1923
|
return stmt(`SELECT 1 AS hit FROM wakes WHERE id = ? AND (${REPEATING_FIRING_PENDING})`).get(id) !== undefined;
|
|
@@ -2028,34 +1927,19 @@ function repeatingFiringPending(id) {
|
|
|
2028
1927
|
}
|
|
2029
1928
|
}
|
|
2030
1929
|
const firingMarker = (timer) => `[hive wake #${timer.id} firing #${timer.fire_count}`;
|
|
2031
|
-
const SOCKET_CONFIRM_GRACE_SECONDS = 60;
|
|
2032
1930
|
const withReDelivered = (note) => (note ? `${note}, re-delivered` : "re-delivered");
|
|
2033
1931
|
const promptEvidenceSql = (lowerBound) => `
|
|
2034
1932
|
SELECT MIN(created_at) FROM agent_state_log
|
|
2035
1933
|
WHERE actor_id = wakes.deliver_actor AND event = 'prompt' AND created_at >= ${lowerBound}
|
|
2036
1934
|
AND (payload LIKE '%[hive wake #' || wakes.id || ']%'
|
|
2037
1935
|
OR payload LIKE '%[hive wake #' || wakes.id || ',%')`;
|
|
2038
|
-
const ENVELOPE_OPEN = '<cross-session-message from="';
|
|
2039
|
-
const ENVELOPE_SENDER_CLOSE = '" from-name="hive">';
|
|
2040
|
-
const EV_PROMPT = "(CASE WHEN json_valid(ev.payload) THEN COALESCE(json_extract(ev.payload, '$.prompt'), '') ELSE ev.payload END)";
|
|
2041
1936
|
const EV_MARKER = "('[hive wake #' || wakes.id || ' firing #' || wakes.fire_count)";
|
|
2042
|
-
const markerAt = (
|
|
2043
|
-
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} || ',')`;
|
|
2044
1938
|
// The current firing's exact marker, at prompt offset 0 (typed) or opening hive's envelope body (socket).
|
|
2045
1939
|
const repeatingEvidenceSql = (select) => `
|
|
2046
1940
|
SELECT ${select} FROM agent_state_log ev
|
|
2047
1941
|
WHERE ev.actor_id = wakes.deliver_actor AND ev.event = 'prompt' AND ev.created_at >= wakes.socket_attempt_at
|
|
2048
|
-
AND
|
|
2049
|
-
OR (${EV_HEADER_END} > ${ENVELOPE_OPEN.length + ENVELOPE_SENDER_CLOSE.length}
|
|
2050
|
-
AND substr(${EV_PROMPT}, 1, ${ENVELOPE_OPEN.length}) = '${ENVELOPE_OPEN}'
|
|
2051
|
-
AND substr(${EV_PROMPT}, ${EV_HEADER_END} - ${ENVELOPE_SENDER_CLOSE.length}, ${ENVELOPE_SENDER_CLOSE.length}) = '${ENVELOPE_SENDER_CLOSE}'
|
|
2052
|
-
AND instr(substr(${EV_PROMPT}, ${ENVELOPE_OPEN.length + 1}, ${EV_HEADER_END} - ${ENVELOPE_OPEN.length + ENVELOPE_SENDER_CLOSE.length + 1}), '"') = 0
|
|
2053
|
-
AND ${markerAt(`${EV_HEADER_END} + 1`)}
|
|
2054
|
-
AND instr(${EV_PROMPT}, char(10) || '${CROSS_SESSION_CLOSE}') > ${EV_HEADER_END}))`;
|
|
2055
|
-
function quietMessagingOn(projectId) {
|
|
2056
|
-
const project = stmt("SELECT path FROM projects WHERE id = ?").get(projectId);
|
|
2057
|
-
return project !== undefined && loadProjectYml(project.path).config?.quiet_messaging === true;
|
|
2058
|
-
}
|
|
1942
|
+
AND ${promptMarkerEvidenceSql(markerAt)}`;
|
|
2059
1943
|
// Only a wake to a live, row-owned Claude lead whose socket was registered by this pane epoch.
|
|
2060
1944
|
function socketRoute(timer, owner) {
|
|
2061
1945
|
return (owner !== null &&
|
|
@@ -2139,7 +2023,7 @@ async function deliver(timer, note, choices, typedSeen, firstHeldAt, owner, fall
|
|
|
2139
2023
|
}
|
|
2140
2024
|
const posted = await postClaudeWake({
|
|
2141
2025
|
socketPath: timer.deliver_messaging_socket,
|
|
2142
|
-
senderAddress:
|
|
2026
|
+
senderAddress: socketSenderAddress(dataDir),
|
|
2143
2027
|
text,
|
|
2144
2028
|
beforeWrite: () => socketStillRegistered(timer, owner),
|
|
2145
2029
|
});
|
|
@@ -2218,15 +2102,7 @@ async function deliver(timer, note, choices, typedSeen, firstHeldAt, owner, fall
|
|
|
2218
2102
|
}
|
|
2219
2103
|
// Measured from the lead's first turn end after the post, because a busy lead reads the post only then.
|
|
2220
2104
|
function socketFallbackDue(timer) {
|
|
2221
|
-
|
|
2222
|
-
let from = firstStop;
|
|
2223
|
-
if (from === null) {
|
|
2224
|
-
const turn = timer.deliver_row_id === null ? null : readLeadTurnState(timer.deliver_row_id);
|
|
2225
|
-
if (turn !== null && turn.state === "working" && turn.pane_pid === timer.deliver_pane_pid)
|
|
2226
|
-
return false;
|
|
2227
|
-
from = timer.socket_attempt_at;
|
|
2228
|
-
}
|
|
2229
|
-
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));
|
|
2230
2106
|
}
|
|
2231
2107
|
async function fallbackClaimed(timer, snapshot, choices) {
|
|
2232
2108
|
const decision = deliverable(timer, snapshot, choices);
|
package/dist/spawn.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { preservedRolloutPath, reapCodexHome } from "./codexHome.js";
|
|
3
3
|
import { getProject } from "./context.js";
|
|
4
|
+
import { LEAD_KIND, LEAD_NAME } from "./agentKinds.js";
|
|
4
5
|
import { gitOut } from "./git.js";
|
|
5
6
|
import { dataDir, db } from "./db.js";
|
|
6
7
|
import { loadProjectYml } from "./projectYml.js";
|
|
@@ -113,13 +114,8 @@ export function asNameClash(e, name) {
|
|
|
113
114
|
}
|
|
114
115
|
return e;
|
|
115
116
|
}
|
|
116
|
-
export
|
|
117
|
-
export const LEAD_ACTOR_PREFIX = `${LEAD_KIND}:`;
|
|
118
|
-
export const isLeadActorId = (actorId) => actorId.startsWith(LEAD_ACTOR_PREFIX);
|
|
119
|
-
export const mintLeadActorId = (agentId) => `${LEAD_ACTOR_PREFIX}${agentId}`;
|
|
117
|
+
export { LEAD_ACTOR_PREFIX, LEAD_KIND, LEAD_NAME, isLeadActorId, isReservedAgentName, mintLeadActorId } from "./agentKinds.js";
|
|
120
118
|
export const isRunningLeadActor = (actorId) => !!db.prepare("SELECT 1 FROM agents WHERE actor_id = ? AND kind = ? AND status = 'running'").get(actorId, LEAD_KIND);
|
|
121
|
-
export const LEAD_NAME = LEAD_KIND;
|
|
122
|
-
export const isReservedAgentName = (name) => name.toLowerCase() === LEAD_NAME;
|
|
123
119
|
export function upsertActor(actorId, name, kind) {
|
|
124
120
|
db.prepare(`INSERT INTO actors (id, name, kind) VALUES (?, ?, ?)
|
|
125
121
|
ON CONFLICT(id) DO UPDATE SET name = excluded.name, last_seen_at = datetime('now')`).run(actorId, name, kind);
|
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/todoScope.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export const TODO_STATUSES = ["open", "in_progress", "backlog", "completed"];
|
|
1
2
|
export const ACTIVE_TODO_SQL = "t.status IN ('open', 'in_progress') AND t.archived_at IS NULL";
|
|
2
3
|
export const NOT_COMPLETED_TODO_SQL = "status != 'completed'";
|
|
3
4
|
export const OPEN_BLOCKERS_FROM_WHERE = `FROM todo_blockers b JOIN todos bt ON bt.id = b.blocker_id
|