toolroll 0.9.97 → 0.9.98
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/CHANGELOG.md +10 -0
- package/dist/approval-authority.d.ts +10 -1
- package/dist/approval-authority.js +18 -2
- package/dist/browser/workspace.js +1 -1
- package/dist/browser-workspace.d.ts +4 -3
- package/dist/chat-actions.d.ts +40 -5
- package/dist/chat-actions.js +124 -13
- package/dist/chat-card.js +4 -1
- package/dist/chat-channel.d.ts +3 -0
- package/dist/chat-channel.js +24 -7
- package/dist/chat-decide.js +4 -2
- package/dist/chat-task-actions.js +1 -1
- package/dist/chat-terms.d.ts +8 -0
- package/dist/chat-terms.js +14 -0
- package/dist/chat-voice.js +1 -1
- package/dist/contracts/chat-actions.d.ts +6 -0
- package/dist/contracts/chat-actions.js +2 -0
- package/dist/contracts/cli.d.ts +6 -0
- package/dist/contracts/cli.js +3 -0
- package/dist/contracts/lead-tools.d.ts +17 -0
- package/dist/contracts/lead-tools.js +4 -1
- package/dist/contracts/store-json.d.ts +1 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/fast-start.d.ts +83 -6
- package/dist/fast-start.js +112 -15
- package/dist/home-groups.d.ts +39 -0
- package/dist/home-groups.js +33 -0
- package/dist/lead-answer-eval.d.ts +7 -0
- package/dist/lead-answer-eval.js +6 -2
- package/dist/lead-capabilities.js +1 -1
- package/dist/lead-context.js +2 -2
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +10 -7
- package/dist/lead-doors.d.ts +32 -0
- package/dist/lead-doors.js +80 -14
- package/dist/lead-status.d.ts +3 -0
- package/dist/lead-status.js +8 -3
- package/dist/lead-tools.d.ts +20 -0
- package/dist/lead-tools.js +66 -3
- package/dist/lead-voice.js +1 -1
- package/dist/lead.js +18 -2
- package/dist/local-owner.js +2 -0
- package/dist/one-step-chat-schema.d.ts +22 -0
- package/dist/one-step-chat-schema.js +37 -0
- package/dist/operate.d.ts +27 -1
- package/dist/operate.js +206 -32
- package/dist/project-delete.js +9 -0
- package/dist/project-knowledge.d.ts +7 -7
- package/dist/publish.d.ts +2 -0
- package/dist/publish.js +1 -1
- package/dist/pull-request-flow.d.ts +4 -0
- package/dist/pull-request-flow.js +16 -1
- package/dist/reply-voice.d.ts +23 -0
- package/dist/reply-voice.js +37 -0
- package/dist/scope.d.ts +6 -1
- package/dist/scope.js +3 -2
- package/dist/server/chat.js +4 -4
- package/dist/server/chrome.js +2 -2
- package/dist/server/home.d.ts +4 -3
- package/dist/server/home.js +33 -28
- package/dist/server/render-chat.js +11 -6
- package/dist/server/render-pages.js +2 -2
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +4 -2
- package/dist/store.js +35 -12
- package/dist/telegram-lead.js +69 -5
- package/dist/telegram-status.js +9 -4
- package/dist/telegram.d.ts +10 -0
- package/dist/telegram.js +92 -8
- package/dist/toolroll-update.js +3 -1
- package/dist/tui-contract.d.ts +2 -0
- package/dist/tui-source.js +3 -0
- package/dist/tui.js +41 -41
- package/dist/typed-start.d.ts +51 -0
- package/dist/typed-start.js +98 -0
- package/dist/wake-hub.d.ts +72 -0
- package/dist/wake-hub.js +151 -0
- package/package.json +1 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v159: one-step start from chat (docs/plans/one-step-chat.md). Adds only: a saved row reads as it did. Kept beside
|
|
3
|
+
* nothing else so the update road can read the lists without the store, and so the version is easy to renumber.
|
|
4
|
+
*
|
|
5
|
+
* - `fast_preview.turn`: the lead turn a chat start's preview was prepared in (fast-start.ts prepareChatStart). Saved
|
|
6
|
+
* rows read NULL; a chat preview without a turn is unusable, and the form's previews never have one.
|
|
7
|
+
* - `chat_typed_start` (C5): a typed yes to a Start card, recorded with the inbound message in the transaction that saves
|
|
8
|
+
* it (typed-start.ts): which card it answered (the proposal and the chat message that carried it), the card's action
|
|
9
|
+
* stamp then, how it was tied to the card (`reply` to it, or the `next` message after it), and once settled, the
|
|
10
|
+
* outcome and the task it started. A new table: nothing saved changes. `proposal` is a plain number, not a reference:
|
|
11
|
+
* the record has to outlive its card. Ending the chat deletes the card's proposal, and a yes already recorded for it
|
|
12
|
+
* must still be answered by Toolroll ("didn't start anything") and never handed to the lead as a new message. Proposal
|
|
13
|
+
* ids are never reused (AUTOINCREMENT), so a kept number can't come to name another card.
|
|
14
|
+
*/
|
|
15
|
+
export declare const V159_SCHEMA = "\nCREATE TABLE IF NOT EXISTS chat_typed_start (\n provider TEXT NOT NULL CHECK (provider IN ('telegram')),\n event TEXT NOT NULL,\n proposal INTEGER NOT NULL,\n card_message TEXT NOT NULL,\n stamp TEXT NOT NULL,\n how TEXT NOT NULL CHECK (how IN ('reply', 'next')),\n created TEXT NOT NULL,\n outcome TEXT CHECK (outcome IS NULL OR outcome IN ('started', 'refused', 'not-counted')),\n task_id TEXT,\n finished TEXT,\n PRIMARY KEY (provider, event),\n FOREIGN KEY (provider, event) REFERENCES chat_event(provider, id)\n);\nCREATE INDEX IF NOT EXISTS chat_typed_start_proposal ON chat_typed_start (proposal);\n";
|
|
16
|
+
export declare const V159_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record<string, {
|
|
17
|
+
definition: string;
|
|
18
|
+
saved: string;
|
|
19
|
+
}>>>>;
|
|
20
|
+
export declare const V159_TABLES: readonly ["chat_typed_start"];
|
|
21
|
+
/** The v159 migration's steps, in order (a test seam may stop after each). */
|
|
22
|
+
export declare const V159_STEPS: readonly ["one-step-chat-tables", "one-step-chat-columns"];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v159: one-step start from chat (docs/plans/one-step-chat.md). Adds only: a saved row reads as it did. Kept beside
|
|
3
|
+
* nothing else so the update road can read the lists without the store, and so the version is easy to renumber.
|
|
4
|
+
*
|
|
5
|
+
* - `fast_preview.turn`: the lead turn a chat start's preview was prepared in (fast-start.ts prepareChatStart). Saved
|
|
6
|
+
* rows read NULL; a chat preview without a turn is unusable, and the form's previews never have one.
|
|
7
|
+
* - `chat_typed_start` (C5): a typed yes to a Start card, recorded with the inbound message in the transaction that saves
|
|
8
|
+
* it (typed-start.ts): which card it answered (the proposal and the chat message that carried it), the card's action
|
|
9
|
+
* stamp then, how it was tied to the card (`reply` to it, or the `next` message after it), and once settled, the
|
|
10
|
+
* outcome and the task it started. A new table: nothing saved changes. `proposal` is a plain number, not a reference:
|
|
11
|
+
* the record has to outlive its card. Ending the chat deletes the card's proposal, and a yes already recorded for it
|
|
12
|
+
* must still be answered by Toolroll ("didn't start anything") and never handed to the lead as a new message. Proposal
|
|
13
|
+
* ids are never reused (AUTOINCREMENT), so a kept number can't come to name another card.
|
|
14
|
+
*/
|
|
15
|
+
export const V159_SCHEMA = `
|
|
16
|
+
CREATE TABLE IF NOT EXISTS chat_typed_start (
|
|
17
|
+
provider TEXT NOT NULL CHECK (provider IN ('telegram')),
|
|
18
|
+
event TEXT NOT NULL,
|
|
19
|
+
proposal INTEGER NOT NULL,
|
|
20
|
+
card_message TEXT NOT NULL,
|
|
21
|
+
stamp TEXT NOT NULL,
|
|
22
|
+
how TEXT NOT NULL CHECK (how IN ('reply', 'next')),
|
|
23
|
+
created TEXT NOT NULL,
|
|
24
|
+
outcome TEXT CHECK (outcome IS NULL OR outcome IN ('started', 'refused', 'not-counted')),
|
|
25
|
+
task_id TEXT,
|
|
26
|
+
finished TEXT,
|
|
27
|
+
PRIMARY KEY (provider, event),
|
|
28
|
+
FOREIGN KEY (provider, event) REFERENCES chat_event(provider, id)
|
|
29
|
+
);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS chat_typed_start_proposal ON chat_typed_start (proposal);
|
|
31
|
+
`;
|
|
32
|
+
export const V159_ADDED_COLUMNS = Object.freeze({
|
|
33
|
+
fast_preview: { turn: { definition: "INTEGER", saved: "NULL" } },
|
|
34
|
+
});
|
|
35
|
+
export const V159_TABLES = Object.freeze(["chat_typed_start"]);
|
|
36
|
+
/** The v159 migration's steps, in order (a test seam may stop after each). */
|
|
37
|
+
export const V159_STEPS = Object.freeze(["one-step-chat-tables", "one-step-chat-columns"]);
|
package/dist/operate.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type TriggerIo } from "./flow-triggers.js";
|
|
|
5
5
|
import { type GhRead } from "./nightly-github.js";
|
|
6
6
|
import type { StepIo } from "./flow-steps.js";
|
|
7
7
|
import type { FetchLike } from "./flow-share.js";
|
|
8
|
-
import { type Store } from "./store.js";
|
|
8
|
+
import { type Store, type Publication } from "./store.js";
|
|
9
9
|
import { type LaptopSeams } from "./laptop-background.js";
|
|
10
10
|
import { type DispatchAdapter } from "./sync.js";
|
|
11
11
|
import { type ParentToRole, type RolePort, type UpRoleStart, type UpStartAuthority } from "./up-processes.js";
|
|
@@ -373,3 +373,29 @@ export declare function upLaptopPortCandidates(store: Store, fromSeam?: readonly
|
|
|
373
373
|
/** The watch loop's background learning pass (docs/plans/learning-loops.md): advisory reads, each project at most every
|
|
374
374
|
* 30 minutes through the plane's own gh. A failure here never reaches the loop: it is tried on the next pass. */
|
|
375
375
|
export declare function refreshLearning(store: Store, now: () => Date, reads?: GhRead): Promise<void>;
|
|
376
|
+
/** A project loop's share in its process's service beats. */
|
|
377
|
+
export type ServiceBeatMember = {
|
|
378
|
+
context: Context;
|
|
379
|
+
progress: (line: string) => void;
|
|
380
|
+
/** This loop's reconciliation stands and it is not stopping: the fast CI pass runs only while every member's does. */
|
|
381
|
+
ciReady: () => boolean;
|
|
382
|
+
};
|
|
383
|
+
/** The delivery beat: web push and outgoing webhooks, off the build path. */
|
|
384
|
+
export declare const DELIVERY_EVERY_MS = 5000;
|
|
385
|
+
/** The fast CI beat: only while an opened pull request waits on its checks or its merge. */
|
|
386
|
+
export declare const FAST_CI_EVERY_MS = 30000;
|
|
387
|
+
/** The most pull requests one fast CI pass reads. */
|
|
388
|
+
export declare const FAST_CI_LIMIT = 10;
|
|
389
|
+
/**
|
|
390
|
+
* The opened pull requests the fast CI pass reads, oldest read or tried (`attempted`) first, at most `limit`: checks not settled (never read,
|
|
391
|
+
* running, or "none" shortly after an update), or green with its merge still waiting (a merge intent in flight, or a
|
|
392
|
+
* followed pull request not yet marked ready on its accepted commit). Failing and settled ones stay on the 5 minute pass.
|
|
393
|
+
*/
|
|
394
|
+
export declare function fastCiWaiting(store: Store, now: Date, limit?: number, attempted?: ReadonlyMap<number, string>): Publication[];
|
|
395
|
+
/**
|
|
396
|
+
* Join this process's service beats: one delivery beat (web push and outgoing webhooks every 5 s, and at once when
|
|
397
|
+
* the wake moves with new notifications) and one fast CI beat (every 30 s, the existing `observeChecks` reader, only
|
|
398
|
+
* for pull requests that wait). One set per store, however many project loops join; the last to leave stops them.
|
|
399
|
+
* Opening a pull request stays on the tick path, and the 5 minute pass alone may set reconciliation ready.
|
|
400
|
+
*/
|
|
401
|
+
export declare function joinServiceBeats(member: ServiceBeatMember): () => Promise<void>;
|
package/dist/operate.js
CHANGED
|
@@ -281,6 +281,7 @@ import { noticeGitIncidents } from "./git-incident.js";
|
|
|
281
281
|
import { containmentNotice, containmentStatus, currentContainment, describeContainment, resolveContainment } from "./containment.js";
|
|
282
282
|
import { readPulls } from "./pulls.js";
|
|
283
283
|
import { startMaintenance } from "./maintenance.js";
|
|
284
|
+
import { WakePoker, wakeHubFor } from "./wake-hub.js";
|
|
284
285
|
import { livePin, modelWatchPass } from "./model-catalog.js";
|
|
285
286
|
import { runModelsCommand } from "./models-cli.js";
|
|
286
287
|
import { ALL_PROJECTS, chatApprovalSettings, chatApprovalWords, setChatApproval } from "./chat-approval.js";
|
|
@@ -6427,8 +6428,22 @@ async function runWatchLoop(args) {
|
|
|
6427
6428
|
// an authority this process no longer holds.
|
|
6428
6429
|
let leaseLost = false;
|
|
6429
6430
|
const followController = new AbortController();
|
|
6431
|
+
// The process's one wake poller (wake-hub.ts) replaces every per-loop and per-lane 50 ms read of the wake counter.
|
|
6432
|
+
// It only says "look": every claim still re-proves its fences.
|
|
6433
|
+
const hub = wakeHubFor(store);
|
|
6434
|
+
const wakes = new WakePoker();
|
|
6430
6435
|
args.onFollowController?.(followController);
|
|
6431
|
-
|
|
6436
|
+
// The loop or a build lane failed (the store could not be read): everything stops, cleans up, and then the first
|
|
6437
|
+
// failure is thrown. Without this a failed lane skipped the cleanup, and a failed loop left its lanes running.
|
|
6438
|
+
let failed = false;
|
|
6439
|
+
let failure;
|
|
6440
|
+
const fail = (error) => {
|
|
6441
|
+
if (!failed)
|
|
6442
|
+
failure = error;
|
|
6443
|
+
failed = true;
|
|
6444
|
+
wakes.poke();
|
|
6445
|
+
};
|
|
6446
|
+
const stopping = () => args.isStopping() || leaseLost || failed;
|
|
6432
6447
|
const heartbeat = setInterval(() => {
|
|
6433
6448
|
let renewed = false;
|
|
6434
6449
|
let problem = "the lease or credential was taken";
|
|
@@ -6441,6 +6456,8 @@ async function runWatchLoop(args) {
|
|
|
6441
6456
|
if (!renewed && !leaseLost) {
|
|
6442
6457
|
leaseLost = true;
|
|
6443
6458
|
followController.abort();
|
|
6459
|
+
// A lost lease ends every doze of this loop and its lanes now, not at the next beat (live-wakeups.md).
|
|
6460
|
+
wakes.poke();
|
|
6444
6461
|
progress(`watch: ${runner} on ${repo}: ${problem} — stopping without admitting more work`);
|
|
6445
6462
|
}
|
|
6446
6463
|
}, WATCH_HEARTBEAT_MS);
|
|
@@ -6495,12 +6512,9 @@ async function runWatchLoop(args) {
|
|
|
6495
6512
|
const deadline = runFor === null ? null : startedAt + runFor;
|
|
6496
6513
|
let lastTick = 0;
|
|
6497
6514
|
let lastBridge = 0;
|
|
6498
|
-
let lastPush = 0;
|
|
6499
|
-
let lastWebhook = 0;
|
|
6500
6515
|
let ticks = 0;
|
|
6501
6516
|
let built = 0;
|
|
6502
6517
|
let brokeCount = 0;
|
|
6503
|
-
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
6504
6518
|
// Reconciliation has its own cadence and sink. Sharing tick's sink would
|
|
6505
6519
|
// interleave envelopes while a build awaits its provider or project check.
|
|
6506
6520
|
const sweepOwner = {};
|
|
@@ -6525,8 +6539,12 @@ async function runWatchLoop(args) {
|
|
|
6525
6539
|
throw new Error(maintenanceLines.join(" "));
|
|
6526
6540
|
if (reconciliationFailure !== null)
|
|
6527
6541
|
progress("watch: reconciliation recovered — new work may resume");
|
|
6542
|
+
const recovered = !reconciliationReady;
|
|
6528
6543
|
reconciliationFailure = null;
|
|
6529
6544
|
reconciliationReady = true;
|
|
6545
|
+
// Admission resumes now: a loop dozing while paused looks again at once.
|
|
6546
|
+
if (recovered)
|
|
6547
|
+
wakes.poke();
|
|
6530
6548
|
if (!stopping() && store.openedPublications().length > 0) {
|
|
6531
6549
|
try {
|
|
6532
6550
|
const checks = await observeChecks(store, {
|
|
@@ -6654,6 +6672,9 @@ async function runWatchLoop(args) {
|
|
|
6654
6672
|
run: async () => { if (await modelWatchPass(store, context.clock()))
|
|
6655
6673
|
progress("watch: checked models and CLI versions"); },
|
|
6656
6674
|
});
|
|
6675
|
+
// The process's delivery and fast CI beats (live-wakeups.md): one set however many project loops run.
|
|
6676
|
+
// The fast CI pass runs only while this loop would admit work: not stopping, reconciled, and no update drain.
|
|
6677
|
+
const leaveServiceBeats = joinServiceBeats({ context, progress, ciReady: () => !admissionStopped() });
|
|
6657
6678
|
// Parallel builds (project-concurrency.ts): this loop is the project's
|
|
6658
6679
|
// first build lane and keeps every housekeeping duty; extra lanes, up to
|
|
6659
6680
|
// the project's own number (never past the worker's capacity), only
|
|
@@ -6729,16 +6750,16 @@ async function runWatchLoop(args) {
|
|
|
6729
6750
|
const seqIdle = store.wakeSeq();
|
|
6730
6751
|
const dozeUntil = Math.min(Date.now() + tickEveryMs, deadline ?? Number.MAX_SAFE_INTEGER);
|
|
6731
6752
|
const theirTurnEnded = () => yieldingTo !== null && yieldingSince !== null && passes.lastPass(yieldingTo) > yieldingSince;
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
}
|
|
6753
|
+
// Doze on the shared poller: a wake, the cadence, a stop, a lost lease or the fair-share turn ending.
|
|
6754
|
+
await hub.changed({ baseline: seqIdle, deadline: dozeUntil, until: () => stopping() || theirTurnEnded(), poker: wakes });
|
|
6735
6755
|
}
|
|
6736
6756
|
};
|
|
6737
6757
|
const ensureLanes = () => {
|
|
6738
6758
|
const facts = slotFacts(null);
|
|
6739
6759
|
const wanted = effectiveConcurrency(facts.limitOf(repo), facts.capacity);
|
|
6760
|
+
// A lane never rejects: its failure is kept, every doze is woken, and the loop's own cleanup runs before it is thrown.
|
|
6740
6761
|
while (lanes.length + 1 < wanted && !stopping())
|
|
6741
|
-
lanes.push(buildLane(lanes.length + 1));
|
|
6762
|
+
lanes.push(buildLane(lanes.length + 1).catch(fail));
|
|
6742
6763
|
};
|
|
6743
6764
|
let firstLaneYieldingSince = null;
|
|
6744
6765
|
let firstLaneYieldingTo = null;
|
|
@@ -6747,7 +6768,9 @@ async function runWatchLoop(args) {
|
|
|
6747
6768
|
await maintenance.runNow();
|
|
6748
6769
|
while (!stopping() && (deadline === null || Date.now() < deadline)) {
|
|
6749
6770
|
if (paused()) {
|
|
6750
|
-
|
|
6771
|
+
// Paused: look again when the wake moves (pause and resume bump it), when reconciliation recovers (a poke),
|
|
6772
|
+
// or after a second at most, whichever comes first.
|
|
6773
|
+
await hub.changed({ baseline: store.wakeSeq(), deadline: Date.now() + 1_000, until: stopping, poker: wakes });
|
|
6751
6774
|
continue;
|
|
6752
6775
|
}
|
|
6753
6776
|
ensureLanes();
|
|
@@ -6817,25 +6840,8 @@ async function runWatchLoop(args) {
|
|
|
6817
6840
|
(published.failed > 0 ? `, gave up on ${published.failed}` : ""));
|
|
6818
6841
|
}
|
|
6819
6842
|
}
|
|
6820
|
-
//
|
|
6821
|
-
//
|
|
6822
|
-
if (now - lastPush >= 45_000) {
|
|
6823
|
-
lastPush = now;
|
|
6824
|
-
try {
|
|
6825
|
-
await pushPass(context.store, { configDir: dirname(context.databaseFile), clock: () => new Date() });
|
|
6826
|
-
}
|
|
6827
|
-
catch {
|
|
6828
|
-
// Push is additive; a broken pass never stops the watch.
|
|
6829
|
-
}
|
|
6830
|
-
}
|
|
6831
|
-
if (now - lastWebhook >= bridgeEveryMs) {
|
|
6832
|
-
lastWebhook = now;
|
|
6833
|
-
const dir = dirname(context.databaseFile);
|
|
6834
|
-
const primary = effectivePrimary(process.env, dir, loadBotToken(process.env, context.telegramTokenFile) !== null);
|
|
6835
|
-
const targets = activeWebhookTargets(process.env, dir, primary.channel);
|
|
6836
|
-
if (targets.length > 0)
|
|
6837
|
-
await webhookPass(store, { targets, consoleUrl: loadConsoleUrl(process.env, dir), clock: context.clock });
|
|
6838
|
-
}
|
|
6843
|
+
// Web push and outgoing webhooks ride the service's own delivery beat (joinServiceBeats, live-wakeups.md):
|
|
6844
|
+
// every 5 s, and at once when the wake moves with new rows, never behind a build on this path.
|
|
6839
6845
|
// The embedded follower owns the wire while it lives; the timer-driven
|
|
6840
6846
|
// pass is the fallback shape for a watch started before a token existed.
|
|
6841
6847
|
if (follower === null && now - lastBridge >= bridgeEveryMs) {
|
|
@@ -6864,7 +6870,7 @@ async function runWatchLoop(args) {
|
|
|
6864
6870
|
// due, a signal lands, or the trial window (--for) ends. Reading one
|
|
6865
6871
|
// integer from SQLite twice a second is the entire idle cost — no
|
|
6866
6872
|
// token has anywhere to be spent from here.
|
|
6867
|
-
const idleUntil = Math.min(lastTick + tickEveryMs, lastBridge + bridgeEveryMs,
|
|
6873
|
+
const idleUntil = Math.min(lastTick + tickEveryMs, lastBridge + bridgeEveryMs, deadline ?? Number.MAX_SAFE_INTEGER) - Date.now();
|
|
6868
6874
|
// Doze for the WHOLE idle window, waking early only for a signal or a
|
|
6869
6875
|
// sequence move — not for one 500ms step and then another pass
|
|
6870
6876
|
// (2026-09-05: that read as two ticks a second, all night, with
|
|
@@ -6872,11 +6878,13 @@ async function runWatchLoop(args) {
|
|
|
6872
6878
|
const seqIdle = store.wakeSeq();
|
|
6873
6879
|
const dozeUntil = Date.now() + Math.max(idleUntil, 0);
|
|
6874
6880
|
const theirTurnEnded = () => firstLaneYieldingTo !== null && firstLaneYieldingSince !== null && passes.lastPass(firstLaneYieldingTo) > firstLaneYieldingSince;
|
|
6875
|
-
|
|
6876
|
-
await sleep(50);
|
|
6877
|
-
}
|
|
6881
|
+
await hub.changed({ baseline: seqIdle, deadline: dozeUntil, until: () => stopping() || theirTurnEnded(), poker: wakes });
|
|
6878
6882
|
}
|
|
6879
6883
|
}
|
|
6884
|
+
catch (error) {
|
|
6885
|
+
// Kept, not thrown yet: the lanes must see the stop, and the cleanup below must run first.
|
|
6886
|
+
fail(error);
|
|
6887
|
+
}
|
|
6880
6888
|
finally {
|
|
6881
6889
|
// Every build lane finishes or hands its attempt back before the
|
|
6882
6890
|
// loop's lease and episode end.
|
|
@@ -6887,6 +6895,7 @@ async function runWatchLoop(args) {
|
|
|
6887
6895
|
await flowMoves.stop();
|
|
6888
6896
|
await flowSteps.stop();
|
|
6889
6897
|
await modelWatch.stop();
|
|
6898
|
+
await leaveServiceBeats();
|
|
6890
6899
|
clearInterval(heartbeat);
|
|
6891
6900
|
followController.abort();
|
|
6892
6901
|
if (follower !== null)
|
|
@@ -6897,6 +6906,8 @@ async function runWatchLoop(args) {
|
|
|
6897
6906
|
store.endWatchEpisode(incarnation, { ticks, built, broke: brokeCount }, new Date());
|
|
6898
6907
|
store.releaseWatchLease(runner, repo, incarnation, new Date());
|
|
6899
6908
|
}
|
|
6909
|
+
if (failed)
|
|
6910
|
+
throw failure;
|
|
6900
6911
|
if (leaseLost) {
|
|
6901
6912
|
return { ok: false, reason: "lease-lost", detail: `the watch lease for ${runner} on ${repo} stopped renewing — another process may have taken this worker over`, ticks, built, broke: brokeCount };
|
|
6902
6913
|
}
|
|
@@ -6966,6 +6977,7 @@ async function watchCommand(flags, context) {
|
|
|
6966
6977
|
}
|
|
6967
6978
|
stopping = true;
|
|
6968
6979
|
followAbort?.abort();
|
|
6980
|
+
wakeHubFor(context.store).poke();
|
|
6969
6981
|
graceTimer = setTimeout(hardStop, stopGraceMs);
|
|
6970
6982
|
};
|
|
6971
6983
|
process.on("SIGINT", stop);
|
|
@@ -12628,6 +12640,8 @@ async function runUpWorkerRole(context, config, port, build) {
|
|
|
12628
12640
|
resolveStopped();
|
|
12629
12641
|
for (const controller of followControllers)
|
|
12630
12642
|
controller.abort();
|
|
12643
|
+
// Every project loop's doze ends now (wake-hub.ts), not at its next beat.
|
|
12644
|
+
wakeHubFor(store).poke();
|
|
12631
12645
|
const building = store.liveClaimCount(runnerName, clock());
|
|
12632
12646
|
if (building > 0) {
|
|
12633
12647
|
progress(`Stopping: ${building} build(s) running. Waiting up to ${Math.round(config.stopGraceMs / 1000)}s for them to finish; any still running then is saved and resumes when the builder starts again.`);
|
|
@@ -14125,3 +14139,163 @@ async function quickAnswersConfig(action, repoGiven, flags, context) {
|
|
|
14125
14139
|
const shown = said();
|
|
14126
14140
|
return succeed(write, json, command, shown.payload, shown.lines);
|
|
14127
14141
|
}
|
|
14142
|
+
/** The delivery beat: web push and outgoing webhooks, off the build path. */
|
|
14143
|
+
export const DELIVERY_EVERY_MS = 5_000;
|
|
14144
|
+
/** The fast CI beat: only while an opened pull request waits on its checks or its merge. */
|
|
14145
|
+
export const FAST_CI_EVERY_MS = 30_000;
|
|
14146
|
+
/** The most pull requests one fast CI pass reads. */
|
|
14147
|
+
export const FAST_CI_LIMIT = 10;
|
|
14148
|
+
/** A pull request whose checks read as "none" stays on the fast pass this long after it was last updated: CI that
|
|
14149
|
+
* has not registered yet, not a project without CI, which the 5 minute pass keeps reading. */
|
|
14150
|
+
const FAST_CI_NONE_WINDOW_MS = 30 * 60_000;
|
|
14151
|
+
const serviceBeats = new WeakMap();
|
|
14152
|
+
/** The newest notification id: the delivery beat's high-water mark. */
|
|
14153
|
+
function newestNotificationId(store) {
|
|
14154
|
+
return Number(store.handle.prepare("SELECT COALESCE(MAX(id), 0) AS id FROM notification").get()?.["id"] ?? 0);
|
|
14155
|
+
}
|
|
14156
|
+
/**
|
|
14157
|
+
* The opened pull requests the fast CI pass reads, oldest read or tried (`attempted`) first, at most `limit`: checks not settled (never read,
|
|
14158
|
+
* running, or "none" shortly after an update), or green with its merge still waiting (a merge intent in flight, or a
|
|
14159
|
+
* followed pull request not yet marked ready on its accepted commit). Failing and settled ones stay on the 5 minute pass.
|
|
14160
|
+
*/
|
|
14161
|
+
export function fastCiWaiting(store, now, limit = FAST_CI_LIMIT, attempted = new Map()) {
|
|
14162
|
+
const waiting = store.openedPublications().filter(publication => {
|
|
14163
|
+
if (publication.prNumber === null)
|
|
14164
|
+
return false;
|
|
14165
|
+
const state = publication.lastCheckState;
|
|
14166
|
+
if (state === null || state === "running")
|
|
14167
|
+
return true;
|
|
14168
|
+
if (state === "none")
|
|
14169
|
+
return now.getTime() - Date.parse(publication.lastCheckAt ?? publication.updatedAt) < FAST_CI_NONE_WINDOW_MS
|
|
14170
|
+
&& now.getTime() - Date.parse(publication.updatedAt) < FAST_CI_NONE_WINDOW_MS;
|
|
14171
|
+
if (state !== "passing")
|
|
14172
|
+
return false;
|
|
14173
|
+
const intent = store.mergeIntentFor(publication.id);
|
|
14174
|
+
if (intent !== null && ["pending", "claimed", "waiting-human", "firing"].includes(intent.state))
|
|
14175
|
+
return true;
|
|
14176
|
+
const follow = store.handle.prepare("SELECT flow_card, ready_head, merge_commit FROM pull_request_follow WHERE publication = ?").get(publication.id);
|
|
14177
|
+
return follow !== undefined && follow["flow_card"] == null && follow["merge_commit"] == null && follow["ready_head"] !== publication.headSha;
|
|
14178
|
+
});
|
|
14179
|
+
// Oldest attempt first: a pull request GitHub cannot read still takes its turn, then goes to the back of the line.
|
|
14180
|
+
const last = (one) => {
|
|
14181
|
+
const read = one.lastCheckAt ?? "", tried = attempted.get(one.id) ?? "";
|
|
14182
|
+
return tried > read ? tried : read;
|
|
14183
|
+
};
|
|
14184
|
+
return waiting
|
|
14185
|
+
.sort((a, b) => last(a).localeCompare(last(b)) || a.id - b.id)
|
|
14186
|
+
.slice(0, limit);
|
|
14187
|
+
}
|
|
14188
|
+
/**
|
|
14189
|
+
* Join this process's service beats: one delivery beat (web push and outgoing webhooks every 5 s, and at once when
|
|
14190
|
+
* the wake moves with new notifications) and one fast CI beat (every 30 s, the existing `observeChecks` reader, only
|
|
14191
|
+
* for pull requests that wait). One set per store, however many project loops join; the last to leave stops them.
|
|
14192
|
+
* Opening a pull request stays on the tick path, and the 5 minute pass alone may set reconciliation ready.
|
|
14193
|
+
*/
|
|
14194
|
+
export function joinServiceBeats(member) {
|
|
14195
|
+
const store = member.context.store;
|
|
14196
|
+
let beats = serviceBeats.get(store);
|
|
14197
|
+
if (beats === undefined)
|
|
14198
|
+
beats = startServiceBeats(store);
|
|
14199
|
+
beats.members.add(member);
|
|
14200
|
+
const joined = beats;
|
|
14201
|
+
return async () => {
|
|
14202
|
+
if (!joined.members.delete(member) || joined.members.size > 0)
|
|
14203
|
+
return;
|
|
14204
|
+
joined.stopped = true;
|
|
14205
|
+
if (serviceBeats.get(store) === joined)
|
|
14206
|
+
serviceBeats.delete(store);
|
|
14207
|
+
joined.poker.poke();
|
|
14208
|
+
await joined.delivery.stop();
|
|
14209
|
+
await joined.fastCi.stop();
|
|
14210
|
+
await joined.wakeLoop;
|
|
14211
|
+
};
|
|
14212
|
+
}
|
|
14213
|
+
function startServiceBeats(store) {
|
|
14214
|
+
const anyMember = () => beats.members.values().next().value;
|
|
14215
|
+
let deliveredThrough = newestNotificationId(store);
|
|
14216
|
+
let deliveryProblem = null;
|
|
14217
|
+
// When the fast CI pass last tried each pull request, read or not.
|
|
14218
|
+
const attempted = new Map();
|
|
14219
|
+
const delivery = startMaintenance({
|
|
14220
|
+
intervalMs: DELIVERY_EVERY_MS,
|
|
14221
|
+
shouldStop: () => beats.stopped,
|
|
14222
|
+
onError: error => {
|
|
14223
|
+
const detail = describe(error);
|
|
14224
|
+
if (detail !== deliveryProblem)
|
|
14225
|
+
anyMember()?.progress(`watch: outgoing webhooks failed — ${detail}; they try again in a few seconds`);
|
|
14226
|
+
deliveryProblem = detail;
|
|
14227
|
+
},
|
|
14228
|
+
run: async () => {
|
|
14229
|
+
const member = anyMember();
|
|
14230
|
+
if (member === undefined)
|
|
14231
|
+
return;
|
|
14232
|
+
const { context } = member;
|
|
14233
|
+
const dir = dirname(context.databaseFile);
|
|
14234
|
+
deliveredThrough = newestNotificationId(store);
|
|
14235
|
+
// Push is additive; a broken pass never stops the watch. The pair ledger's claims fence any other sender.
|
|
14236
|
+
try {
|
|
14237
|
+
await pushPass(store, { configDir: dir, clock: () => new Date() });
|
|
14238
|
+
}
|
|
14239
|
+
catch { /* tried again next beat */ }
|
|
14240
|
+
const primary = effectivePrimary(process.env, dir, loadBotToken(process.env, context.telegramTokenFile) !== null);
|
|
14241
|
+
const targets = activeWebhookTargets(process.env, dir, primary.channel);
|
|
14242
|
+
if (targets.length > 0)
|
|
14243
|
+
await webhookPass(store, { targets, consoleUrl: loadConsoleUrl(process.env, dir), clock: context.clock });
|
|
14244
|
+
deliveryProblem = null;
|
|
14245
|
+
},
|
|
14246
|
+
});
|
|
14247
|
+
const fastCi = startMaintenance({
|
|
14248
|
+
intervalMs: FAST_CI_EVERY_MS,
|
|
14249
|
+
shouldStop: () => beats.stopped,
|
|
14250
|
+
// A failed fast pass is skipped: it records nothing and says nothing. The 5 minute pass still reads every PR.
|
|
14251
|
+
onError: () => { },
|
|
14252
|
+
run: async () => {
|
|
14253
|
+
// Every project loop must still stand, read again before each revision, ready mark and merge: a stop, a lost
|
|
14254
|
+
// lease, a failed reconciliation or an update drain that arrives while GitHub answers ends the pass there.
|
|
14255
|
+
const standing = () => !beats.stopped && beats.members.size > 0 && [...beats.members].every(one => one.ciReady());
|
|
14256
|
+
if (!standing())
|
|
14257
|
+
return;
|
|
14258
|
+
const { context, progress } = beats.members.values().next().value;
|
|
14259
|
+
const now = context.clock();
|
|
14260
|
+
const waiting = fastCiWaiting(store, now, FAST_CI_LIMIT, attempted);
|
|
14261
|
+
if (waiting.length === 0)
|
|
14262
|
+
return;
|
|
14263
|
+
// A pull request GitHub would not read keeps its place in line only until the others had their turn.
|
|
14264
|
+
if (attempted.size > 500)
|
|
14265
|
+
attempted.clear();
|
|
14266
|
+
for (const one of waiting)
|
|
14267
|
+
attempted.set(one.id, now.toISOString());
|
|
14268
|
+
const exec = context.publishExec === undefined ? {} : { exec: context.publishExec };
|
|
14269
|
+
const checks = await observeChecks(store, { ...exec, publications: waiting });
|
|
14270
|
+
if (!standing())
|
|
14271
|
+
return;
|
|
14272
|
+
if (checks.failing > 0)
|
|
14273
|
+
progress(`watch: CI is red on ${checks.failing} published PR(s) — the outbox has it`);
|
|
14274
|
+
const followed = await followPullRequests(store, checks.seen, { evidenceRoot: context.evidenceRoot, ...exec, proceed: standing });
|
|
14275
|
+
if (followed.revisions > 0)
|
|
14276
|
+
progress(`watch: filed ${followed.revisions} CI revision(s)`);
|
|
14277
|
+
if (followed.merged > 0)
|
|
14278
|
+
progress(`watch: merged ${followed.merged} pull request(s) whose checks passed`);
|
|
14279
|
+
},
|
|
14280
|
+
});
|
|
14281
|
+
const poker = new WakePoker();
|
|
14282
|
+
const hub = wakeHubFor(store);
|
|
14283
|
+
const beats = { members: new Set(), stopped: false, delivery, fastCi, poker, wakeLoop: Promise.resolve() };
|
|
14284
|
+
// A wake with new notifications sends them now, not at the next beat.
|
|
14285
|
+
beats.wakeLoop = (async () => {
|
|
14286
|
+
while (!beats.stopped) {
|
|
14287
|
+
try {
|
|
14288
|
+
const why = await hub.changed({ baseline: store.wakeSeq(), deadline: Number.MAX_SAFE_INTEGER, until: () => beats.stopped, poker });
|
|
14289
|
+
if (why === "wake" && !beats.stopped && newestNotificationId(store) > deliveredThrough)
|
|
14290
|
+
void delivery.runNow();
|
|
14291
|
+
}
|
|
14292
|
+
catch {
|
|
14293
|
+
// The store could not be read: the 5 s beat still runs; look again shortly.
|
|
14294
|
+
await new Promise(resolveLater => { const timer = setTimeout(resolveLater, 1_000); timer.unref?.(); });
|
|
14295
|
+
}
|
|
14296
|
+
}
|
|
14297
|
+
})();
|
|
14298
|
+
serviceBeats.set(store, beats);
|
|
14299
|
+
void delivery.runNow();
|
|
14300
|
+
return beats;
|
|
14301
|
+
}
|
package/dist/project-delete.js
CHANGED
|
@@ -239,6 +239,15 @@ function deleteRows(store, repo, d, now) {
|
|
|
239
239
|
del("chat_decide_action", IN("task_id"), K);
|
|
240
240
|
del("chat_decide_prompt", IN("task_id"), K);
|
|
241
241
|
del("chat_message_ref", `${IN("decision")} OR ${IN("task_id")} OR ${IN("run")} OR project = ? OR ${IN("notification")} OR ${IN("task_ref")}`, decisions, K, R, repo, notifications, T);
|
|
242
|
+
// A typed yes to a Start card (one-step-chat.md C5). Its record goes with its message. Where the message stays (it
|
|
243
|
+
// named no task of this project) but the card's proposal goes, the record is settled instead and forgets the task: a
|
|
244
|
+
// yes still waiting is then answered by Toolroll as one that started nothing, never handed to the lead.
|
|
245
|
+
del("chat_typed_start", `provider || ':' || event IN (SELECT value FROM json_each(?))`, conversations);
|
|
246
|
+
if (tableExists(db, "chat_typed_start")) {
|
|
247
|
+
db.prepare(`UPDATE chat_typed_start SET outcome = 'not-counted', task_id = NULL, finished = COALESCE(finished, ?) WHERE ${IN("proposal")}`).run(now.toISOString(), proposals);
|
|
248
|
+
// A yes that started one of this project's tasks, said in a message that stays: the record forgets the task.
|
|
249
|
+
db.prepare(`UPDATE chat_typed_start SET task_id = NULL WHERE ${IN("task_id")}`).run(K);
|
|
250
|
+
}
|
|
242
251
|
del("chat_part", `provider || ':' || id IN (${parts})`, conversations, K, R);
|
|
243
252
|
del("chat_event", `provider || ':' || id IN (SELECT value FROM json_each(?))`, conversations);
|
|
244
253
|
del("chat_card_task", IN("task_ref"), T);
|
|
@@ -22,17 +22,20 @@ export type KnowledgeView = ReturnType<typeof knowledgeView>;
|
|
|
22
22
|
export declare function knowledgeVersion(store: Store, repo: string, actor: string, revision: number): Knowledge;
|
|
23
23
|
/** Chat reads only an admitted project. Keep the full source in the project UI. */
|
|
24
24
|
export declare function conversationKnowledge(store: Store, repo: string, actor: string, reference?: string, decision?: number): {
|
|
25
|
+
content?: never;
|
|
26
|
+
history?: never;
|
|
25
27
|
revision: number;
|
|
26
28
|
stale: boolean;
|
|
27
29
|
notice: string;
|
|
28
30
|
title?: never;
|
|
29
|
-
content?: never;
|
|
30
|
-
history?: never;
|
|
31
31
|
instructions?: never;
|
|
32
32
|
references?: never;
|
|
33
33
|
decisions?: never;
|
|
34
34
|
waiting?: never;
|
|
35
35
|
} | {
|
|
36
|
+
content?: never;
|
|
37
|
+
revision?: never;
|
|
38
|
+
history?: never;
|
|
36
39
|
stale?: never;
|
|
37
40
|
decision: {
|
|
38
41
|
repo: string;
|
|
@@ -51,28 +54,25 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
51
54
|
afterwards?: string;
|
|
52
55
|
notice: string;
|
|
53
56
|
title?: never;
|
|
54
|
-
content?: never;
|
|
55
|
-
revision?: never;
|
|
56
|
-
history?: never;
|
|
57
57
|
instructions?: never;
|
|
58
58
|
references?: never;
|
|
59
59
|
decisions?: never;
|
|
60
60
|
waiting?: never;
|
|
61
61
|
} | {
|
|
62
|
+
history?: never;
|
|
62
63
|
stale?: never;
|
|
63
64
|
revision: number;
|
|
64
65
|
title: string;
|
|
65
66
|
content: string;
|
|
66
67
|
notice: string;
|
|
67
|
-
history?: never;
|
|
68
68
|
instructions?: never;
|
|
69
69
|
references?: never;
|
|
70
70
|
decisions?: never;
|
|
71
71
|
waiting?: never;
|
|
72
72
|
} | {
|
|
73
|
+
content?: never;
|
|
73
74
|
stale?: never;
|
|
74
75
|
title?: never;
|
|
75
|
-
content?: never;
|
|
76
76
|
revision: number;
|
|
77
77
|
history: {
|
|
78
78
|
revision: number;
|
package/dist/publish.d.ts
CHANGED
|
@@ -107,6 +107,8 @@ export type ObservedPullRequest = {
|
|
|
107
107
|
export declare function observeChecks(store: Store, options: {
|
|
108
108
|
clock?: () => Date;
|
|
109
109
|
exec?: PublishExec;
|
|
110
|
+
/** Read only these (the fast pass's waiting pull requests, live-wakeups.md); every one is read exactly as below. */
|
|
111
|
+
publications?: readonly Publication[];
|
|
110
112
|
}): Promise<CheckReport>;
|
|
111
113
|
export declare const MERGE_LEASE_MS: number;
|
|
112
114
|
export declare const FIRING_GRACE_MS: number;
|
package/dist/publish.js
CHANGED
|
@@ -304,7 +304,7 @@ export async function observeChecks(store, options) {
|
|
|
304
304
|
const clock = options.clock ?? (() => new Date());
|
|
305
305
|
const exec = options.exec ?? execRun;
|
|
306
306
|
const report = { observed: 0, failing: 0, resolved: 0, problems: [], seen: [] };
|
|
307
|
-
for (const publication of store.openedPublications()) {
|
|
307
|
+
for (const publication of options.publications ?? store.openedPublications()) {
|
|
308
308
|
if (publication.prNumber === null)
|
|
309
309
|
continue;
|
|
310
310
|
// The observation generation is RESERVED before the network call
|
|
@@ -129,6 +129,8 @@ export declare function followPullRequests(store: Store, seen: readonly Observed
|
|
|
129
129
|
evidenceRoot: string;
|
|
130
130
|
exec?: PublishExec;
|
|
131
131
|
clock?: () => Date;
|
|
132
|
+
/** Asked again before each revision, ready mark and merge: false ends the pass there and writes nothing more. */
|
|
133
|
+
proceed?: () => boolean;
|
|
132
134
|
}): Promise<FollowReport>;
|
|
133
135
|
/** A person's merge: their password first, then the same checked merge. */
|
|
134
136
|
export declare function mergeAsPerson(store: Store, input: {
|
|
@@ -156,6 +158,8 @@ export declare function mergePullRequest(store: Store, input: {
|
|
|
156
158
|
method?: MergeMethod;
|
|
157
159
|
/** Only a person merging by hand may skip waiting for the full check. */
|
|
158
160
|
anyway?: boolean;
|
|
161
|
+
/** An automatic pass's standing, read in the write that reserves the merge: false refuses ("halted") and merges nothing. */
|
|
162
|
+
proceed?: () => boolean;
|
|
159
163
|
}): Promise<Result<{
|
|
160
164
|
commit: string | null;
|
|
161
165
|
}>>;
|
|
@@ -285,7 +285,10 @@ export async function followPullRequests(store, seen, options) {
|
|
|
285
285
|
const exec = options.exec ?? execRun;
|
|
286
286
|
const clock = options.clock ?? (() => new Date());
|
|
287
287
|
const report = { revisions: 0, asked: 0, ready: 0, merged: 0, problems: [] };
|
|
288
|
+
const halted = () => options.proceed?.() === false;
|
|
288
289
|
for (const one of seen) {
|
|
290
|
+
if (halted())
|
|
291
|
+
break;
|
|
289
292
|
const publication = one.publication;
|
|
290
293
|
const follow = followOf(store, publication.id);
|
|
291
294
|
// A flow's Pull request zone follows its own PR: green moves the card on, red takes its failure path.
|
|
@@ -313,6 +316,8 @@ export async function followPullRequests(store, seen, options) {
|
|
|
313
316
|
const check = failingChecks(one.rollup)[0] ?? { name: "a check", url: null };
|
|
314
317
|
const skeleton = ciRevisionNote(check.name, pr, one.headOid, "");
|
|
315
318
|
const excerpt = await logExcerpt(exec, publication.githubRepo, check.url, 490 - skeleton.length);
|
|
319
|
+
if (halted())
|
|
320
|
+
break;
|
|
316
321
|
const note = ciRevisionNote(check.name, pr, one.headOid, excerpt);
|
|
317
322
|
const request = createHash("sha256").update(`ci-revision ${publication.githubRepo} ${pr} ${one.headOid}`).digest("hex").slice(0, 32);
|
|
318
323
|
const scope = store.getScope(taskId);
|
|
@@ -348,11 +353,17 @@ export async function followPullRequests(store, seen, options) {
|
|
|
348
353
|
const grant = store.publicationGrantFor(store.refById(publication.taskRef)?.repo ?? "");
|
|
349
354
|
updateFollow(store, publication.id, { ready_head: one.headOid }, clock());
|
|
350
355
|
if (grant?.mergeWhenGreen === true && grant.publishOn === "complete") {
|
|
351
|
-
const merged = await mergePullRequest(store, { runId: publication.run, by: "merge when checks pass", exec, clock
|
|
356
|
+
const merged = await mergePullRequest(store, { runId: publication.run, by: "merge when checks pass", exec, clock,
|
|
357
|
+
...(options.proceed === undefined ? {} : { proceed: options.proceed }) });
|
|
352
358
|
if (merged.ok) {
|
|
353
359
|
report.merged++;
|
|
354
360
|
continue;
|
|
355
361
|
}
|
|
362
|
+
// Halted before the merge was reserved: the ready mark is taken back so the next pass merges it, and nobody is asked to.
|
|
363
|
+
if (merged.reason === "halted") {
|
|
364
|
+
updateFollow(store, publication.id, { ready_head: follow.readyHead }, clock());
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
356
367
|
report.problems.push(`PR #${pr}: ${merged.message}`);
|
|
357
368
|
}
|
|
358
369
|
store.enqueueNotification({
|
|
@@ -485,6 +496,8 @@ export async function mergePullRequest(store, input) {
|
|
|
485
496
|
// One merge at a time, each owning its reservation by its token: a second caller is refused, and each releases only its own.
|
|
486
497
|
const token = randomUUID();
|
|
487
498
|
const reserved = store.transact(() => {
|
|
499
|
+
if (input.proceed?.() === false)
|
|
500
|
+
return "halted";
|
|
488
501
|
if (refArchived(store, publication.taskRef))
|
|
489
502
|
return "archived";
|
|
490
503
|
if (store.handle.prepare("SELECT 1 FROM hold WHERE task_ref = ? AND owner_id LIKE 'project-delete:%' AND (until IS NULL OR until > ?)").get(publication.taskRef, clock().toISOString()) !== undefined)
|
|
@@ -494,6 +507,8 @@ export async function mergePullRequest(store, input) {
|
|
|
494
507
|
store.handle.prepare("INSERT OR REPLACE INTO merge_in_flight (publication, task_ref, token, by, at) VALUES (?, ?, ?, ?, ?)").run(publication.id, publication.taskRef, token, input.by, clock().toISOString());
|
|
495
508
|
return "ok";
|
|
496
509
|
});
|
|
510
|
+
if (reserved === "halted")
|
|
511
|
+
return refuse("halted", "The worker is stopping or paused, so this merge waits for its next pass.");
|
|
497
512
|
if (reserved === "archived")
|
|
498
513
|
return refuse("archived", "This task is put away. Restore it before merging.");
|
|
499
514
|
if (reserved === "deleting")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type ReplyVoice = {
|
|
2
|
+
/** Characters of the shaped reply. */
|
|
3
|
+
characters: number;
|
|
4
|
+
sentences: number;
|
|
5
|
+
/** The `PLAIN_WORDS` rows said outside code, plus "owner" when the reply calls the person "the owner". */
|
|
6
|
+
internalWords: string[];
|
|
7
|
+
/** Opens with its own steps or a filler word ("I checked", "Let me", "Sure,") instead of the answer. */
|
|
8
|
+
narratedOpening: boolean;
|
|
9
|
+
};
|
|
10
|
+
/** A reply longer than this is counted as long in the eval's report. */
|
|
11
|
+
export declare const LONG_REPLY_CHARACTERS = 320;
|
|
12
|
+
export declare function replyVoice(reply: string): ReplyVoice;
|
|
13
|
+
export type VoiceSummary = {
|
|
14
|
+
answers: number;
|
|
15
|
+
medianCharacters: number;
|
|
16
|
+
/** Share of answers over `LONG_REPLY_CHARACTERS`, from 0 to 1. */
|
|
17
|
+
longShare: number;
|
|
18
|
+
/** Internal words said, summed over the answers. */
|
|
19
|
+
internalWords: number;
|
|
20
|
+
narratedOpenings: number;
|
|
21
|
+
};
|
|
22
|
+
/** One model's answers, summed up for the report. */
|
|
23
|
+
export declare function summarizeVoice(voices: readonly ReplyVoice[]): VoiceSummary;
|