superagent-claude 0.1.0 → 0.1.1
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/lib/cli.js +2 -2
- package/lib/{main-Br6CBO2s.js → main-DrCQeHQM.js} +604 -576
- package/package.json +5 -2
|
@@ -13,9 +13,12 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
13
13
|
import { checkSync, lockSync } from "proper-lockfile";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
// package.json
|
|
16
|
-
var version = "0.1.
|
|
16
|
+
var version = "0.1.1";
|
|
17
17
|
// src/infra/env.ts
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Environment for subprocesses: only the inherited whitelist (PATH, HOME, LANG) plus explicitly named variables.
|
|
20
|
+
* The daemon environment is never passed whole.
|
|
21
|
+
*/
|
|
19
22
|
const INHERITED_ENV = [
|
|
20
23
|
"PATH",
|
|
21
24
|
"HOME",
|
|
@@ -30,7 +33,7 @@ function subprocessEnv(base, explicit = {}) {
|
|
|
30
33
|
return Object.assign(result, explicit);
|
|
31
34
|
}
|
|
32
35
|
// src/runtimes/claude/mcp.ts
|
|
33
|
-
/**
|
|
36
|
+
/** Exposes the host ToolSpecs as an in-process SDK MCP server named `superagent`, so names and validation match. */
|
|
34
37
|
const MCP_SERVER = "superagent";
|
|
35
38
|
const mcpToolName = (name) => `mcp__${MCP_SERVER}__${name}`;
|
|
36
39
|
function toolHandler(spec, signal) {
|
|
@@ -106,7 +109,7 @@ const SUBAGENT_TYPE = `Only these subagent types are allowed: ${SUBAGENT_TYPES.j
|
|
|
106
109
|
const SUBAGENT_ISOLATION = "Subagent isolation is not allowed";
|
|
107
110
|
const SUBAGENT_NESTED = "A subagent cannot start another subagent";
|
|
108
111
|
const SUBAGENT_HOST_TOOL = "Host tools are only available to the main worker, not to subagents";
|
|
109
|
-
/** Turns off
|
|
112
|
+
/** Turns off CLI commit and PR attribution through the flag settings layer; settingSources stays as it is. */
|
|
110
113
|
const ATTRIBUTION_OFF = {
|
|
111
114
|
attribution: {
|
|
112
115
|
commit: "",
|
|
@@ -372,18 +375,16 @@ const LIMITS = {
|
|
|
372
375
|
}
|
|
373
376
|
};
|
|
374
377
|
// src/policy/admission.ts
|
|
375
|
-
/** Default admission rules (matching pre-table behavior): unaddressed messages from listed senders are inferred candidates; unlisted senders are ignored. */
|
|
376
378
|
const ADMISSION_DEFAULTS = {
|
|
377
379
|
listedUnaddressed: "inferred",
|
|
378
380
|
unlistedAddressed: "ignore",
|
|
379
381
|
unlistedUnaddressed: "ignore"
|
|
380
382
|
};
|
|
381
383
|
// src/core/model.ts
|
|
382
|
-
/** Stable key of a thread across the whole domain. */
|
|
383
384
|
const threadKey = (ref) => `${ref.source}:${ref.id}`;
|
|
384
385
|
/**
|
|
385
|
-
* Normalizes valid timestamps to `Date#toISOString` form so stored timestamps compare lexicographically in memory and
|
|
386
|
-
* values that do not parse as a timestamp are returned unchanged.
|
|
386
|
+
* Normalizes valid timestamps to `Date#toISOString` form so stored timestamps compare lexicographically in memory and
|
|
387
|
+
* SQL; values that do not parse as a timestamp are returned unchanged.
|
|
387
388
|
*/
|
|
388
389
|
function isoTime(value) {
|
|
389
390
|
const time = Date.parse(value);
|
|
@@ -391,10 +392,10 @@ function isoTime(value) {
|
|
|
391
392
|
}
|
|
392
393
|
/**
|
|
393
394
|
* The tickets whose worker decides a person's follow-up: those it reached on their own Threads that can still decide it
|
|
394
|
-
* and did not hand it back. A ticket can while it is live, also once a person dropped its Subscription to the Thread,
|
|
395
|
-
* while it is ended, not by a person, with the Trigger waiting to resume it. None once a person sent it to Eval, or
|
|
396
|
-
* an Eval holds it, which an earlier version may have queued. While a ticket owns it, the message goes to no
|
|
397
|
-
* main Session may only stop the work on it.
|
|
395
|
+
* and did not hand it back. A ticket can while it is live, also once a person dropped its Subscription to the Thread,
|
|
396
|
+
* and while it is ended, not by a person, with the Trigger waiting to resume it. None once a person sent it to Eval, or
|
|
397
|
+
* while an Eval holds it, which an earlier version may have queued. While a ticket owns it, the message goes to no
|
|
398
|
+
* Eval, and the main Session may only stop the work on it.
|
|
398
399
|
*/
|
|
399
400
|
function followupOwners(trigger, records) {
|
|
400
401
|
if (!trigger.human) return [];
|
|
@@ -409,22 +410,21 @@ function followupOwners(trigger, records) {
|
|
|
409
410
|
});
|
|
410
411
|
}
|
|
411
412
|
/**
|
|
412
|
-
* Whether a Decision settled what a person's message asks for: a stop on it, taken or refused, decides only that
|
|
413
|
-
* work, routing it to a ticket leaves it to that ticket's worker, and an unavailable one decided nothing.
|
|
413
|
+
* Whether a Decision settled what a person's message asks for: a stop on it, taken or refused, decides only that
|
|
414
|
+
* ticket's work, routing it to a ticket leaves it to that ticket's worker, and an unavailable one decided nothing.
|
|
414
415
|
*/
|
|
415
416
|
const settled = (decision) => !["stop", "unavailable"].includes(decision.kind) && !decision.reason.some((reason) => [
|
|
416
417
|
"stopNotExplicit",
|
|
417
418
|
"stopUnknown",
|
|
418
419
|
"routed"
|
|
419
420
|
].includes(reason.code));
|
|
420
|
-
/** Whether a ticket is open: it has not ended. */
|
|
421
421
|
const liveTicket = (ticket) => !ticket.ended;
|
|
422
|
-
/** How a ticket ended. */
|
|
423
422
|
const ticketOutcome = (ticket) => ticket.ended?.outcome;
|
|
424
423
|
const runningAssignment = (ticket) => ticket.assignments.find((assignment) => assignment.state === "running");
|
|
425
424
|
/**
|
|
426
|
-
* Whether a Trigger reached a ticket: it started the ticket, came to it as an Input, or is on a Thread the
|
|
427
|
-
* to; with `context` false, not a Thread the ticket follows only for context, where its
|
|
425
|
+
* Whether a Trigger reached a ticket: it started the ticket, came to it as an Input, or is on a Thread the
|
|
426
|
+
* ticket subscribes to; with `context` false, not a Thread the ticket follows only for context, where its
|
|
427
|
+
* worker may read but not write.
|
|
428
428
|
*/
|
|
429
429
|
function reached(ticket, trigger, thread, context = true) {
|
|
430
430
|
if (ticket.origin.triggerId === trigger.id) return true;
|
|
@@ -435,25 +435,28 @@ function reached(ticket, trigger, thread, context = true) {
|
|
|
435
435
|
}) || input && (context || !trigger.threads.every((ref) => thread(threadKey(ref))?.context?.includes(ticket.id)));
|
|
436
436
|
}
|
|
437
437
|
const latestAssignment = (ticket) => ticket.assignments.at(-1);
|
|
438
|
-
/** The latest report any of a ticket's Assignments made, which is what the ticket last said. */
|
|
439
438
|
const latestReport = (ticket) => ticket.assignments.findLast((assignment) => assignment.report)?.report;
|
|
440
439
|
/**
|
|
441
|
-
* Until when a ticket's worker had a turn to read what reached it: its latest report, but a progress report when
|
|
442
|
-
* is false, or the end of its latest turn, whichever is later.
|
|
440
|
+
* Until when a ticket's worker had a turn to read what reached it: its latest report, but a progress report when
|
|
441
|
+
* `progress` is false, or the end of its latest turn, whichever is later.
|
|
443
442
|
*/
|
|
444
443
|
function readUntil(ticket, progress = true) {
|
|
445
444
|
return [(ticket.assignments.findLast((assignment) => assignment.report && (progress || assignment.report.state !== "progress"))?.report)?.at ?? "", ...ticket.assignments.map((assignment) => assignment.idleAt ?? "")].reduce((a, b) => a > b ? a : b);
|
|
446
445
|
}
|
|
447
446
|
/**
|
|
448
|
-
* Whether a ticket waits for a person: its latest Assignment reported blocked or failed, and no person's
|
|
449
|
-
* since. A later report that is neither, a person's message, or a retry, which starts a
|
|
447
|
+
* Whether a ticket waits for a person: its latest Assignment reported blocked or failed, and no person's
|
|
448
|
+
* message reached it since. A later report that is neither, a person's message, or a retry, which starts a
|
|
449
|
+
* new Assignment, lifts it.
|
|
450
450
|
*/
|
|
451
451
|
function waitsForPerson(ticket) {
|
|
452
452
|
const report = latestAssignment(ticket)?.report;
|
|
453
453
|
if (report?.state !== "blocked" && report?.state !== "failed") return false;
|
|
454
454
|
return !ticket.inputs.some((input) => input.kind === "trigger" && input.human && !input.context && input.at > report.at);
|
|
455
455
|
}
|
|
456
|
-
/**
|
|
456
|
+
/**
|
|
457
|
+
* Whether a running Assignment's worker finished with everything delivered to it: a turn ended (idleAt) at or after the
|
|
458
|
+
* last delivery, and no Trigger Input delivered to it is unread or was read after that turn.
|
|
459
|
+
*/
|
|
457
460
|
function caughtUp(ticket, assignment) {
|
|
458
461
|
const at = assignment.idleAt;
|
|
459
462
|
if (!at || (assignment.deliveredAt ?? "") > at) return false;
|
|
@@ -507,10 +510,7 @@ const USER_ERRORS = {
|
|
|
507
510
|
noLabels: ["refused", "This Source has no names to refresh"],
|
|
508
511
|
nothingToSync: ["refused", "The {source} Source's part of the Policy has nothing to sync from, or the Source is off or waiting for setup"]
|
|
509
512
|
};
|
|
510
|
-
/**
|
|
511
|
-
* Structured error thrown by Engine parts and application services when a caller request is invalid or references missing state.
|
|
512
|
-
* Tool handlers and HTTP routes inspect its code and missing flag to return appropriate status responses, while the web panel localizes the code.
|
|
513
|
-
*/
|
|
513
|
+
/** Tool handlers and HTTP routes map `missing` to a status; the web panel localizes `code`. */
|
|
514
514
|
var UserError = class extends Error {
|
|
515
515
|
code;
|
|
516
516
|
values;
|
|
@@ -524,15 +524,14 @@ var UserError = class extends Error {
|
|
|
524
524
|
}
|
|
525
525
|
};
|
|
526
526
|
// src/core/ids.ts
|
|
527
|
-
/**
|
|
527
|
+
/** Equal inputs always produce the same id. */
|
|
528
528
|
function key(...values) {
|
|
529
529
|
return createHash("sha256").update(JSON.stringify(values)).digest("hex").slice(0, 32);
|
|
530
530
|
}
|
|
531
531
|
// src/core/part.ts
|
|
532
|
-
/** Shared base class and configuration for Engine domain parts. */
|
|
533
532
|
/**
|
|
534
|
-
*
|
|
535
|
-
*
|
|
533
|
+
* Subclasses mutate their own aggregates and delegate cross-aggregate changes to sibling parts, within the
|
|
534
|
+
* transaction Engine opens.
|
|
536
535
|
*/
|
|
537
536
|
var EnginePart = class {
|
|
538
537
|
repo;
|
|
@@ -547,7 +546,6 @@ var EnginePart = class {
|
|
|
547
546
|
policy() {
|
|
548
547
|
return this.options.policy();
|
|
549
548
|
}
|
|
550
|
-
/** The tickets whose worker decides a person's follow-up: see `followupOwners`. */
|
|
551
549
|
owners(trigger) {
|
|
552
550
|
return followupOwners(trigger, this.repo);
|
|
553
551
|
}
|
|
@@ -557,9 +555,9 @@ var EnginePart = class {
|
|
|
557
555
|
if (owners.length) throw new UserError("workerDecides", { tickets: owners.join(", ") });
|
|
558
556
|
}
|
|
559
557
|
/**
|
|
560
|
-
* Record that a ticket's worker hands back a Trigger that reached the ticket, with why, and queue it for Eval when
|
|
561
|
-
* ticket decided it and no other ticket still does. One the ticket did not decide, such as a bot's, one a person
|
|
562
|
-
* Eval or one on a Thread it follows for context, went to Eval without it.
|
|
558
|
+
* Record that a ticket's worker hands back a Trigger that reached the ticket, with why, and queue it for Eval when
|
|
559
|
+
* the ticket decided it and no other ticket still does. One the ticket did not decide, such as a bot's, one a person
|
|
560
|
+
* sent to Eval or one on a Thread it follows for context, went to Eval without it.
|
|
563
561
|
* @returns whether it was queued.
|
|
564
562
|
*/
|
|
565
563
|
handBack(ticketId, trigger, reason) {
|
|
@@ -579,7 +577,6 @@ var EnginePart = class {
|
|
|
579
577
|
});
|
|
580
578
|
return decided && !this.owners(trigger).length && this.queueForEval(trigger);
|
|
581
579
|
}
|
|
582
|
-
/** Record that a ticket's worker took a person's message up as within the ticket, by acknowledging or answering it. */
|
|
583
580
|
takeUp(ticketId, triggerId) {
|
|
584
581
|
const disposition = this.repo.triggers.disposition(triggerId);
|
|
585
582
|
if (disposition && !disposition.acked?.includes(ticketId)) this.repo.triggers.saveDisposition(triggerId, {
|
|
@@ -587,7 +584,6 @@ var EnginePart = class {
|
|
|
587
584
|
acked: [...disposition.acked ?? [], ticketId]
|
|
588
585
|
});
|
|
589
586
|
}
|
|
590
|
-
/** Queue a Trigger on its primary Thread for the next Eval, unless it waits there already or an Eval holds it. */
|
|
591
587
|
queueForEval(trigger) {
|
|
592
588
|
const primary = this.thread(trigger.threads[0]);
|
|
593
589
|
if (primary.pending.includes(trigger.id) || primary.evaluating?.triggerIds.includes(trigger.id)) return false;
|
|
@@ -637,8 +633,7 @@ var EnginePart = class {
|
|
|
637
633
|
}
|
|
638
634
|
};
|
|
639
635
|
// src/core/stage.ts
|
|
640
|
-
/**
|
|
641
|
-
/** Classifies a ticket's stage; the rules follow the table in docs/003 in order, where the first matching row wins. */
|
|
636
|
+
/** Rules are checked in order; the first match wins. */
|
|
642
637
|
function ticketStage(ticket) {
|
|
643
638
|
if (ticket.ended) return { stage: "ended" };
|
|
644
639
|
if (ticket.delivery) return {
|
|
@@ -666,28 +661,21 @@ function ticketStage(ticket) {
|
|
|
666
661
|
if (report === "waiting" || assignment.state === "ended") return { stage: "idle" };
|
|
667
662
|
return { stage: "working" };
|
|
668
663
|
}
|
|
669
|
-
/** True when an open ticket is stalled on a blocked/failed report without a newer human follow-up, or has a failed input delivery. */
|
|
670
664
|
function canRetry(ticket) {
|
|
671
665
|
return !ticket.ended && (waitsForPerson(ticket) || !!ticket.delivery);
|
|
672
666
|
}
|
|
673
667
|
// src/core/tickets.ts
|
|
674
|
-
/** Tickets: their lifecycle from an accept to their end, their Subscriptions and their Inputs. */
|
|
675
668
|
const sameChannel = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
676
|
-
/** Whether an Input is a message no person wrote, such as CI's, a bot's, or the main Session's note. */
|
|
677
669
|
const automatic = (input) => input?.kind === "note" || input?.kind === "trigger" && !input.human;
|
|
678
670
|
/**
|
|
679
|
-
* Whether a ticket's worker read a Trigger Input before `until`, its latest report or turn end: its Session
|
|
680
|
-
* into a step before then. One delivered before reads were recorded counts as read once it
|
|
671
|
+
* Whether a ticket's worker read a Trigger Input before `until`, its latest report or turn end: its Session
|
|
672
|
+
* took the message into a step before then. One delivered before reads were recorded counts as read once it
|
|
673
|
+
* reached the worker by then.
|
|
681
674
|
*/
|
|
682
675
|
const readBefore = (input, until) => input.readAt === void 0 ? (input.deliveredAt && input.deliveredAt > input.at ? input.deliveredAt : input.at) <= until : !!input.readAt && input.readAt < until;
|
|
683
676
|
const STATUS_REQUEST = "Report the current ticket status with orchestrator_report. This is a status-only request: do not resume implementation or perform external actions.";
|
|
684
677
|
const LIMIT_CONTINUE_NOTE = "Your previous run stopped at its turn or budget limit. A person replied, so the limits start over. Inspect the worktree and remote state, continue the assignment where it stopped, and report its status.";
|
|
685
|
-
/**
|
|
686
|
-
* Manages each ticket's lifecycle: creates the ticket for an accept decision with its Mandate, Subscriptions, and first
|
|
687
|
-
* Assignment, binds and attaches Threads, adds Inputs, resumes, retries, restarts, and ends tickets, and drops Subscriptions.
|
|
688
|
-
* Coordinates with Replies for the acknowledgement an accept carries and with Executor to start, deliver to, and stop assignments.
|
|
689
|
-
* Intake, Evals, Assignments, and Engine delegate ticket and subscription mutations here.
|
|
690
|
-
*/
|
|
678
|
+
/** Coordinates with Replies for an accept's acknowledgement, and with the Executor to start, deliver, and stop. */
|
|
691
679
|
var Tickets = class extends EnginePart {
|
|
692
680
|
replies;
|
|
693
681
|
constructor(options, replies) {
|
|
@@ -695,9 +683,8 @@ var Tickets = class extends EnginePart {
|
|
|
695
683
|
this.replies = replies;
|
|
696
684
|
}
|
|
697
685
|
/**
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
* gave the Eval its strength, or the Eval's last Trigger for an Eval recorded before `primary` existed.
|
|
686
|
+
* The acknowledgement an accept carries is held until its Assignment starts. The origin is the admissible Trigger
|
|
687
|
+
* that gave the Eval its strength, or the Eval's last Trigger for an Eval recorded before `primary` existed.
|
|
701
688
|
*/
|
|
702
689
|
create(id, decisionId, mandate, evaluation, outcome, at) {
|
|
703
690
|
const { triggerIds } = evaluation;
|
|
@@ -762,8 +749,8 @@ var Tickets = class extends EnginePart {
|
|
|
762
749
|
return ticket;
|
|
763
750
|
}
|
|
764
751
|
/**
|
|
765
|
-
* Record that a Thread open tickets follow closed, and end each one done with its Threads (see `followsDone`), unless
|
|
766
|
-
* work has not finished (see `unfinished`); such work ends the ticket once it reports ready.
|
|
752
|
+
* Record that a Thread open tickets follow closed, and end each one done with its Threads (see `followsDone`), unless
|
|
753
|
+
* its work has not finished (see `unfinished`); such work ends the ticket once it reports ready.
|
|
767
754
|
*/
|
|
768
755
|
threadClosed(thread, outcome, at) {
|
|
769
756
|
const closed = threadKey(thread);
|
|
@@ -782,8 +769,9 @@ var Tickets = class extends EnginePart {
|
|
|
782
769
|
}
|
|
783
770
|
}
|
|
784
771
|
/**
|
|
785
|
-
* How a ticket that follows Threads is done with them: once no Thread it opened (`own`) is still open, or, when it
|
|
786
|
-
* once every one it follows closed. It ends as the latest close among those Threads did; undefined while
|
|
772
|
+
* How a ticket that follows Threads is done with them: once no Thread it opened (`own`) is still open, or, when it
|
|
773
|
+
* opened none, once every one it follows closed. It ends as the latest close among those Threads did; undefined while
|
|
774
|
+
* it is not done.
|
|
787
775
|
*/
|
|
788
776
|
followsDone(ticket) {
|
|
789
777
|
const follows = ticket.follows ?? [];
|
|
@@ -793,8 +781,8 @@ var Tickets = class extends EnginePart {
|
|
|
793
781
|
return counted.map((item) => item.closed).sort((a, b) => a.at.localeCompare(b.at)).at(-1);
|
|
794
782
|
}
|
|
795
783
|
/**
|
|
796
|
-
*
|
|
797
|
-
*
|
|
784
|
+
* @throws UserError when the trigger or ticket is unknown, a ticket's worker still decides the trigger, a person
|
|
785
|
+
* stopped the ticket, or an ended ticket's Mandate no longer revalidates.
|
|
798
786
|
*/
|
|
799
787
|
attachable(ticketId, triggerId) {
|
|
800
788
|
const trigger = this.repo.triggers.get(triggerId);
|
|
@@ -806,9 +794,9 @@ var Tickets = class extends EnginePart {
|
|
|
806
794
|
return trigger;
|
|
807
795
|
}
|
|
808
796
|
/**
|
|
809
|
-
* Subscribe a ticket to an attached Trigger's Threads and add the Trigger as an Input, with its note. The ticket's
|
|
810
|
-
* then decides it, as it decides a message that reached the ticket on its own Thread; a worker that does not
|
|
811
|
-
* message as its ticket's takes it as a new Input, which wakes an ended ticket as any attached message does.
|
|
797
|
+
* Subscribe a ticket to an attached Trigger's Threads and add the Trigger as an Input, with its note. The ticket's
|
|
798
|
+
* worker then decides it, as it decides a message that reached the ticket on its own Thread; a worker that does not
|
|
799
|
+
* hold the message as its ticket's takes it as a new Input, which wakes an ended ticket as any attached message does.
|
|
812
800
|
*/
|
|
813
801
|
attach(ticketId, trigger, note) {
|
|
814
802
|
for (const ref of trigger.threads) this.subscribe(ticketId, ref);
|
|
@@ -859,16 +847,14 @@ var Tickets = class extends EnginePart {
|
|
|
859
847
|
});
|
|
860
848
|
return true;
|
|
861
849
|
}
|
|
862
|
-
/**
|
|
863
|
-
* Stops a ticket on behalf of a human, ending any active assignments.
|
|
864
|
-
* @throws UserError when the ticket is unknown.
|
|
865
|
-
*/
|
|
850
|
+
/** @throws UserError when the ticket is unknown. */
|
|
866
851
|
stop(ticketId, reason) {
|
|
867
852
|
this.end(ticketId, "human", "stopped", this.now(), reason);
|
|
868
853
|
return this.require(ticketId);
|
|
869
854
|
}
|
|
870
855
|
/**
|
|
871
|
-
* Ends the stalled assignment of a retryable open ticket (`failed`, `blocked`, or failed input delivery) and starts a
|
|
856
|
+
* Ends the stalled assignment of a retryable open ticket (`failed`, `blocked`, or failed input delivery) and starts a
|
|
857
|
+
* new one.
|
|
872
858
|
* @throws UserError when the ticket is unknown or not retryable.
|
|
873
859
|
*/
|
|
874
860
|
retry(ticketId) {
|
|
@@ -885,8 +871,9 @@ var Tickets = class extends EnginePart {
|
|
|
885
871
|
return ticket;
|
|
886
872
|
}
|
|
887
873
|
/**
|
|
888
|
-
* Drops a ticket's Subscription to a Thread its worker found unrelated, and hands back, with `reason`, the person's
|
|
889
|
-
* @throws UserError when the ticket is unknown, or `thread` is an origin
|
|
874
|
+
* Drops a ticket's Subscription to a Thread its worker found unrelated, and hands back, with `reason`, the person's
|
|
875
|
+
* messages there that the ticket decides. @throws UserError when the ticket is unknown, or `thread` is an origin
|
|
876
|
+
* thread, a bound `follows` thread, or not subscribed.
|
|
890
877
|
*/
|
|
891
878
|
markUnrelated(ticketId, thread, reason) {
|
|
892
879
|
const ticket = this.require(ticketId);
|
|
@@ -897,8 +884,8 @@ var Tickets = class extends EnginePart {
|
|
|
897
884
|
for (const trigger of this.triggersOf(ticket)) if (trigger.threads.some((ref) => threadKey(ref) === thread) && this.owners(trigger).includes(ticketId)) this.handBack(ticketId, trigger, reason);
|
|
898
885
|
}
|
|
899
886
|
/**
|
|
900
|
-
* Tell each open ticket a Policy change bears on how it does, oldest first, so their deliveries queue in the order
|
|
901
|
-
* were accepted; the others are left alone.
|
|
887
|
+
* Tell each open ticket a Policy change bears on how it does, oldest first, so their deliveries queue in the order
|
|
888
|
+
* they were accepted; the others are left alone.
|
|
902
889
|
*/
|
|
903
890
|
policyChanged(from, to, effects) {
|
|
904
891
|
const at = this.now();
|
|
@@ -915,9 +902,9 @@ var Tickets = class extends EnginePart {
|
|
|
915
902
|
}
|
|
916
903
|
}
|
|
917
904
|
/**
|
|
918
|
-
* Drop a Thread's Subscriptions, every ticket's or only `ticketId`'s, and say so; false when none dropped. A ticket
|
|
919
|
-
* followed the Thread follows it no more, so its close no longer ends the ticket. Each ticket keeps deciding the
|
|
920
|
-
* it received from the Thread.
|
|
905
|
+
* Drop a Thread's Subscriptions, every ticket's or only `ticketId`'s, and say so; false when none dropped. A ticket
|
|
906
|
+
* that followed the Thread follows it no more, so its close no longer ends the ticket. Each ticket keeps deciding the
|
|
907
|
+
* messages it received from the Thread.
|
|
921
908
|
*/
|
|
922
909
|
dropSubscriptions(thread, by, ticketId, reason) {
|
|
923
910
|
const current = this.repo.threads.get(thread);
|
|
@@ -944,9 +931,9 @@ var Tickets = class extends EnginePart {
|
|
|
944
931
|
return true;
|
|
945
932
|
}
|
|
946
933
|
/**
|
|
947
|
-
* Add an Input once (idempotently) and ask the Executor to deliver it, or to launch again a pending Assignment whose
|
|
948
|
-
* failed once no person is waited for: a Policy change alone does not launch it again. A message no person
|
|
949
|
-
* ticket that rests on a ready report and goes with the next person's message (see `restsReady`).
|
|
934
|
+
* Add an Input once (idempotently) and ask the Executor to deliver it, or to launch again a pending Assignment whose
|
|
935
|
+
* launch failed once no person is waited for: a Policy change alone does not launch it again. A message no person
|
|
936
|
+
* wrote waits on a ticket that rests on a ready report and goes with the next person's message (see `restsReady`).
|
|
950
937
|
*/
|
|
951
938
|
addInput(ticket, input) {
|
|
952
939
|
if (ticket.inputs.some((existing) => existing.id === input.id)) return;
|
|
@@ -971,10 +958,11 @@ var Tickets = class extends EnginePart {
|
|
|
971
958
|
else if (latest?.state === "pending" && !waitsForPerson(ticket)) this.options.executor.start(ticket, latest);
|
|
972
959
|
}
|
|
973
960
|
/**
|
|
974
|
-
* Resume an ended ticket under its unchanged Mandate when a person's message on one of its Threads waits for it: no
|
|
975
|
-
* and no Decision, since nothing new is authorized. One whose Mandate the current Policy no longer holds stays
|
|
976
|
-
* takes nothing (see `refuse`). While processing is paused the message waits, and the first pass after the
|
|
977
|
-
* the ticket under the Policy then. Reads the ticket afresh, so it is idempotent and leaves an
|
|
961
|
+
* Resume an ended ticket under its unchanged Mandate when a person's message on one of its Threads waits for it: no
|
|
962
|
+
* Eval and no Decision, since nothing new is authorized. One whose Mandate the current Policy no longer holds stays
|
|
963
|
+
* ended and takes nothing (see `refuse`). While processing is paused the message waits, and the first pass after the
|
|
964
|
+
* pause resumes the ticket under the Policy then. Reads the ticket afresh, so it is idempotent and leaves an
|
|
965
|
+
* already-resumed ticket alone.
|
|
978
966
|
*/
|
|
979
967
|
wake(ticketId) {
|
|
980
968
|
const ticket = this.repo.tickets.get(ticketId);
|
|
@@ -1006,43 +994,45 @@ var Tickets = class extends EnginePart {
|
|
|
1006
994
|
triggerId: first.triggerId
|
|
1007
995
|
});
|
|
1008
996
|
}
|
|
1009
|
-
/** The Triggers of a ticket's trigger Inputs, or of those of them whose Input ids are `ids`. */
|
|
1010
997
|
triggersOf(ticket, ids) {
|
|
1011
998
|
return ticket.inputs.flatMap((input) => input.kind === "trigger" && (!ids || ids.includes(input.id)) ? this.repo.triggers.get(input.triggerId) ?? [] : []);
|
|
1012
999
|
}
|
|
1013
1000
|
/**
|
|
1014
|
-
* Whether a ticket's worker took a person's message up, by acknowledging or answering it. A message acknowledged
|
|
1015
|
-
* acknowledgements were recorded on it has only the acknowledgement's message in the outbox.
|
|
1001
|
+
* Whether a ticket's worker took a person's message up, by acknowledging or answering it. A message acknowledged
|
|
1002
|
+
* before acknowledgements were recorded on it has only the acknowledgement's message in the outbox.
|
|
1016
1003
|
*/
|
|
1017
1004
|
takenUp(ticket, trigger) {
|
|
1018
1005
|
const acked = this.repo.triggers.disposition(trigger.id)?.acked;
|
|
1019
1006
|
return acked ? acked.includes(ticket.id) : this.options.outbox.status(key("ack", trigger.id)) !== void 0;
|
|
1020
1007
|
}
|
|
1021
|
-
/** The person's messages among a ticket's Inputs `ids` that it decides and its worker did not take up. */
|
|
1022
1008
|
untaken(ticket, ids) {
|
|
1023
1009
|
return this.triggersOf(ticket, ids).filter((trigger) => this.owners(trigger).includes(ticket.id) && !this.takenUp(ticket, trigger));
|
|
1024
1010
|
}
|
|
1025
|
-
/** The person's messages on its own Threads that reached a ticket after its latest report, when that report is `ready`; undefined otherwise. */
|
|
1026
1011
|
sinceReady(ticket) {
|
|
1027
1012
|
const report = latestReport(ticket);
|
|
1028
1013
|
if (report?.state !== "ready") return void 0;
|
|
1029
1014
|
return this.triggersOf(ticket, ticket.inputs.filter((input) => input.kind === "trigger" && input.human && !input.context && input.at > report.at).map((input) => input.id));
|
|
1030
1015
|
}
|
|
1031
|
-
/**
|
|
1016
|
+
/**
|
|
1017
|
+
* Whether a ticket keeps its `ready` report through a limit stop: its worker took up none of the
|
|
1018
|
+
* person's messages since.
|
|
1019
|
+
*/
|
|
1032
1020
|
keepsReady(ticket) {
|
|
1033
1021
|
const since = this.sinceReady(ticket);
|
|
1034
1022
|
return !!since && !since.some((trigger) => this.takenUp(ticket, trigger));
|
|
1035
1023
|
}
|
|
1036
|
-
/**
|
|
1024
|
+
/**
|
|
1025
|
+
* Whether a ticket rests on its `ready` report: every person's message since was handed back by it and not taken up.
|
|
1026
|
+
*/
|
|
1037
1027
|
restsReady(ticket) {
|
|
1038
1028
|
const since = this.sinceReady(ticket);
|
|
1039
1029
|
return !!since && since.every((trigger) => !this.takenUp(ticket, trigger) && !!this.repo.triggers.disposition(trigger.id)?.handbacks?.some((item) => item.ticketId === ticket.id));
|
|
1040
1030
|
}
|
|
1041
1031
|
/**
|
|
1042
|
-
* A ticket that ended, or cannot resume, lets go of the messages its worker never received, `waiting`, and of the
|
|
1043
|
-
* messages it decided but may not have taken up, `left`: it no longer counts as having reached them, and
|
|
1044
|
-
* that no other ticket decides goes to Eval, as a person's message that reached no ticket does, unless
|
|
1045
|
-
* up, a Decision settled it, or it is the message a stop ended this ticket on.
|
|
1032
|
+
* A ticket that ended, or cannot resume, lets go of the messages its worker never received, `waiting`, and of the
|
|
1033
|
+
* person's messages it decided but may not have taken up, `left`: it no longer counts as having reached them, and
|
|
1034
|
+
* each of `left` that no other ticket decides goes to Eval, as a person's message that reached no ticket does, unless
|
|
1035
|
+
* a ticket took it up, a Decision settled it, or it is the message a stop ended this ticket on.
|
|
1046
1036
|
*/
|
|
1047
1037
|
letGo(ticketId, waiting, left) {
|
|
1048
1038
|
for (const trigger of [...waiting, ...left]) {
|
|
@@ -1060,14 +1050,12 @@ var Tickets = class extends EnginePart {
|
|
|
1060
1050
|
if (!this.owners(trigger).length && !disposition?.acked?.length && !decided) this.queueForEval(trigger);
|
|
1061
1051
|
}
|
|
1062
1052
|
}
|
|
1063
|
-
/** An ended ticket that cannot resume takes none of the messages that waited for it (see `letGo`). */
|
|
1064
1053
|
refuse(ticket) {
|
|
1065
1054
|
const waiting = this.triggersOf(ticket, ticket.undelivered), left = this.untaken(ticket, ticket.undelivered);
|
|
1066
1055
|
ticket.undelivered = [];
|
|
1067
1056
|
this.repo.tickets.save(ticket);
|
|
1068
1057
|
this.letGo(ticket.id, waiting, left);
|
|
1069
1058
|
}
|
|
1070
|
-
/** Replace a ticket's running Assignment with a new one that receives `note` first, and queue it for launch. */
|
|
1071
1059
|
restart(ticket, running, reason, note) {
|
|
1072
1060
|
this.endAssignment(ticket, running, reason);
|
|
1073
1061
|
this.requeueUntaken(ticket, running);
|
|
@@ -1086,10 +1074,10 @@ var Tickets = class extends EnginePart {
|
|
|
1086
1074
|
this.options.executor.start(ticket, assignment);
|
|
1087
1075
|
}
|
|
1088
1076
|
/**
|
|
1089
|
-
* Put back, ahead of what waits, the Trigger Inputs delivered to `replaced` that no step of its worker took: a cancel
|
|
1090
|
-
* have dropped them, or a step that claimed them was cut off, and the new Assignment's launch takes a copy still
|
|
1091
|
-
* in the Session out, so its delivery is the only one. An earlier end let go of what earlier Assignments
|
|
1092
|
-
* A message the ticket handed back is not its work and is not put back.
|
|
1077
|
+
* Put back, ahead of what waits, the Trigger Inputs delivered to `replaced` that no step of its worker took: a cancel
|
|
1078
|
+
* may have dropped them, or a step that claimed them was cut off, and the new Assignment's launch takes a copy still
|
|
1079
|
+
* waiting in the Session out, so its delivery is the only one. An earlier end let go of what earlier Assignments
|
|
1080
|
+
* left. A message the ticket handed back is not its work and is not put back.
|
|
1093
1081
|
*/
|
|
1094
1082
|
requeueUntaken(ticket, replaced) {
|
|
1095
1083
|
const untaken = ticket.inputs.filter((input) => input.kind === "trigger" && input.readAt === null && input.deliveredTo === replaced.id && !ticket.undelivered.includes(input.id) && !this.repo.triggers.disposition(input.triggerId)?.handbacks?.some((item) => item.ticketId === ticket.id));
|
|
@@ -1097,10 +1085,11 @@ var Tickets = class extends EnginePart {
|
|
|
1097
1085
|
}
|
|
1098
1086
|
/**
|
|
1099
1087
|
* End a ticket with its outcome. `at` records when the ending fact happened, such as a PR merge, and `reason` says
|
|
1100
|
-
* why in words, by default the outcome. The ticket lets go (see `letGo`) of the messages its worker never received,
|
|
1101
|
-
* the person's messages it decides that its worker did not read before its latest report or turn end, the
|
|
1102
|
-
* ends the ticket included (see `readBefore`), and neither acknowledged, answered nor handed back: one
|
|
1103
|
-
* the worker wrote the report that ends the ticket, or after its turn ended, and either way it
|
|
1088
|
+
* why in words, by default the outcome. The ticket lets go (see `letGo`) of the messages its worker never received,
|
|
1089
|
+
* and of the person's messages it decides that its worker did not read before its latest report or turn end, the
|
|
1090
|
+
* report that ends the ticket included (see `readBefore`), and neither acknowledged, answered nor handed back: one
|
|
1091
|
+
* may have come while the worker wrote the report that ends the ticket, or after its turn ended, and either way it
|
|
1092
|
+
* can no longer hand it back.
|
|
1104
1093
|
*/
|
|
1105
1094
|
end(ticketId, by, outcome, at = this.now(), reason) {
|
|
1106
1095
|
const ticket = this.require(ticketId);
|
|
@@ -1135,10 +1124,10 @@ var Tickets = class extends EnginePart {
|
|
|
1135
1124
|
for (const assignment of stopping) this.options.executor.stop(ticket, assignment, reason ?? outcome);
|
|
1136
1125
|
}
|
|
1137
1126
|
/**
|
|
1138
|
-
* Whether a ticket's work has not finished at a close at `at`: its worker took up a person's message the ticket
|
|
1139
|
-
* has not reported other than progress, or ended a turn, since its Session took that message into a step,
|
|
1140
|
-
* after the close, whose Assignment started after it, has no ready report or Input arrived after that
|
|
1141
|
-
* change asks for no work.
|
|
1127
|
+
* Whether a ticket's work has not finished at a close at `at`: its worker took up a person's message the ticket
|
|
1128
|
+
* decides and has not reported other than progress, or ended a turn, since its Session took that message into a step,
|
|
1129
|
+
* or work resumed after the close, whose Assignment started after it, has no ready report or Input arrived after that
|
|
1130
|
+
* report; a Policy change asks for no work.
|
|
1142
1131
|
*/
|
|
1143
1132
|
unfinished(ticket, at) {
|
|
1144
1133
|
const until = readUntil(ticket, false);
|
|
@@ -1171,7 +1160,8 @@ var Tickets = class extends EnginePart {
|
|
|
1171
1160
|
});
|
|
1172
1161
|
}
|
|
1173
1162
|
/**
|
|
1174
|
-
* Give an open ticket's Executor a fact as a note Input, such as what another ticket found; it grants nothing (see
|
|
1163
|
+
* Give an open ticket's Executor a fact as a note Input, such as what another ticket found; it grants nothing (see
|
|
1164
|
+
* `TicketInput`).
|
|
1175
1165
|
* @throws UserError when the ticket is unknown or has ended.
|
|
1176
1166
|
*/
|
|
1177
1167
|
note(ticketId, text) {
|
|
@@ -1187,7 +1177,6 @@ var Tickets = class extends EnginePart {
|
|
|
1187
1177
|
return this.require(ticketId);
|
|
1188
1178
|
}
|
|
1189
1179
|
/**
|
|
1190
|
-
* Subscribe an open ticket to a Thread its Executor found related, for context: see `Thread.context`.
|
|
1191
1180
|
* @returns false when the ticket follows the Thread already.
|
|
1192
1181
|
* @throws UserError when the ticket is unknown or has ended.
|
|
1193
1182
|
*/
|
|
@@ -1200,7 +1189,6 @@ var Tickets = class extends EnginePart {
|
|
|
1200
1189
|
this.repo.threads.save(thread);
|
|
1201
1190
|
return true;
|
|
1202
1191
|
}
|
|
1203
|
-
/** Subscribe a ticket to a Thread; later Triggers on it reach the ticket as Inputs, as the ticket's own once it was for context. */
|
|
1204
1192
|
subscribe(ticketId, ref) {
|
|
1205
1193
|
const thread = this.thread(ref);
|
|
1206
1194
|
if (!thread.subscriptions.includes(ticketId)) {
|
|
@@ -1215,15 +1203,8 @@ var Tickets = class extends EnginePart {
|
|
|
1215
1203
|
}
|
|
1216
1204
|
};
|
|
1217
1205
|
// src/core/assignments.ts
|
|
1218
|
-
/** What the Executor and its workers report about the Assignments that run tickets. */
|
|
1219
1206
|
const RECOVERY_NOTE = "The host restarted during this ticket. Inspect the current worktree, git history and remote state before continuing. Do not repeat an already completed external operation. Finish the original assignment and report its status.";
|
|
1220
|
-
/**
|
|
1221
|
-
* Assignments records what the Executor and its workers report about the Assignments that run tickets: one started, Inputs
|
|
1222
|
-
* reached it or failed to, its worker's Session took Inputs into a step, a turn of its worker ended, a host restart
|
|
1223
|
-
* interrupted it, and a worker's report with its question for a person. It depends on
|
|
1224
|
-
* Tickets to restart or end a ticket and on Replies to withdraw a question a report replaced; the Engine routes the
|
|
1225
|
-
* Executor's lifecycle callbacks and its workers' reports here.
|
|
1226
|
-
*/
|
|
1207
|
+
/** Depends on Tickets to restart or end a ticket and on Replies to withdraw a question a report replaced. */
|
|
1227
1208
|
var Assignments = class extends EnginePart {
|
|
1228
1209
|
tickets;
|
|
1229
1210
|
replies;
|
|
@@ -1232,7 +1213,6 @@ var Assignments = class extends EnginePart {
|
|
|
1232
1213
|
this.tickets = tickets;
|
|
1233
1214
|
this.replies = replies;
|
|
1234
1215
|
}
|
|
1235
|
-
/** Marks a non-ended assignment running, queues a held accept acknowledgement (ADR-32), queues delivery of waiting undelivered inputs on an open ticket, and emits `assignment.started`. */
|
|
1236
1216
|
started(ticketId, assignmentId) {
|
|
1237
1217
|
const ticket = this.require(ticketId);
|
|
1238
1218
|
const assignment = ticket.assignments.find((item) => item.id === assignmentId);
|
|
@@ -1253,10 +1233,7 @@ var Assignments = class extends EnginePart {
|
|
|
1253
1233
|
assignmentId
|
|
1254
1234
|
});
|
|
1255
1235
|
}
|
|
1256
|
-
/**
|
|
1257
|
-
* Marks `inputIds` delivered on the ticket's running assignment and clears any prior `delivery` failure; ignored if no
|
|
1258
|
-
* longer running. Its worker reads each once its Session takes it into a step (see `read`).
|
|
1259
|
-
*/
|
|
1236
|
+
/** Reads are recorded later by `read`, once the worker's Session takes the input into a step. */
|
|
1260
1237
|
delivered(ticketId, assignmentId, inputIds) {
|
|
1261
1238
|
const ticket = this.running(ticketId, assignmentId);
|
|
1262
1239
|
if (!ticket) return;
|
|
@@ -1270,7 +1247,6 @@ var Assignments = class extends EnginePart {
|
|
|
1270
1247
|
delete ticket.delivery;
|
|
1271
1248
|
this.repo.tickets.save(ticket);
|
|
1272
1249
|
}
|
|
1273
|
-
/** Records when the worker's Session of an open ticket took `inputIds` into a step, from which its model reads them. */
|
|
1274
1250
|
read(ticketId, inputIds) {
|
|
1275
1251
|
const ticket = this.require(ticketId);
|
|
1276
1252
|
if (!liveTicket(ticket)) return;
|
|
@@ -1284,9 +1260,9 @@ var Assignments = class extends EnginePart {
|
|
|
1284
1260
|
this.repo.tickets.save(ticket);
|
|
1285
1261
|
}
|
|
1286
1262
|
/**
|
|
1287
|
-
* A worker's Session stopped at its turn or budget limit (`limit`). A ticket whose latest report is `ready` and whose
|
|
1288
|
-
* took up no person's message since keeps that report: the running Assignment carries it, so the ticket shows
|
|
1289
|
-
* review, and the caller reports nothing
|
|
1263
|
+
* A worker's Session stopped at its turn or budget limit (`limit`). A ticket whose latest report is `ready` and whose
|
|
1264
|
+
* worker took up no person's message since keeps that report: the running Assignment carries it, so the ticket shows
|
|
1265
|
+
* as ready for review, and the caller reports nothing. Emits `worker.limit` either way.
|
|
1290
1266
|
* @returns whether the ticket kept its ready report; false when `assignmentId` no longer runs the open ticket.
|
|
1291
1267
|
*/
|
|
1292
1268
|
limited(ticketId, assignmentId, limit) {
|
|
@@ -1317,9 +1293,9 @@ var Assignments = class extends EnginePart {
|
|
|
1317
1293
|
this.repo.tickets.save(ticket);
|
|
1318
1294
|
}
|
|
1319
1295
|
/**
|
|
1320
|
-
* Handles a running assignment interrupted by a host restart: restarts it with a recovery note unless its latest
|
|
1321
|
-
* `waiting`, `ready`, `blocked`, or `failed` and no input reached its worker after that report that a later
|
|
1322
|
-
* did not take in
|
|
1296
|
+
* Handles a running assignment interrupted by a host restart: restarts it with a recovery note unless its latest
|
|
1297
|
+
* report is `waiting`, `ready`, `blocked`, or `failed` and no input reached its worker after that report that a later
|
|
1298
|
+
* finished turn did not take in, in which case it keeps its session and flushes any waiting undelivered inputs.
|
|
1323
1299
|
*/
|
|
1324
1300
|
interrupted(ticketId, assignmentId) {
|
|
1325
1301
|
const ticket = this.running(ticketId, assignmentId);
|
|
@@ -1336,10 +1312,8 @@ var Assignments = class extends EnginePart {
|
|
|
1336
1312
|
else if (ticket.undelivered.length) this.options.executor.deliver(ticket, running);
|
|
1337
1313
|
}
|
|
1338
1314
|
/**
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
* @throws UserError for an unknown or ended ticket, an ended assignment, a question combined with a ready result or answer,
|
|
1342
|
-
* or a question/answer referencing a non-human or unreached trigger.
|
|
1315
|
+
* @throws UserError for an unknown or ended ticket, an ended assignment, a question combined with a ready result or
|
|
1316
|
+
* answer, or a question/answer referencing a non-human or unreached trigger.
|
|
1343
1317
|
*/
|
|
1344
1318
|
report(ticketId, report) {
|
|
1345
1319
|
const ticket = this.require(ticketId);
|
|
@@ -1387,7 +1361,10 @@ var Assignments = class extends EnginePart {
|
|
|
1387
1361
|
if (outcome) this.tickets.end(ticketId, "executor", outcome, at);
|
|
1388
1362
|
return this.require(ticketId);
|
|
1389
1363
|
}
|
|
1390
|
-
/**
|
|
1364
|
+
/**
|
|
1365
|
+
* The web address of the ticket's own thread, from its last own follow: recorded at bind time, else the
|
|
1366
|
+
* thread's own address.
|
|
1367
|
+
*/
|
|
1391
1368
|
ownPullUrl(ticket) {
|
|
1392
1369
|
const follow = ticket.follows?.findLast((item) => item.own);
|
|
1393
1370
|
if (!follow) return void 0;
|
|
@@ -1395,17 +1372,13 @@ var Assignments = class extends EnginePart {
|
|
|
1395
1372
|
const url = this.repo.threads.get(follow.thread)?.meta.url;
|
|
1396
1373
|
return typeof url === "string" ? url : void 0;
|
|
1397
1374
|
}
|
|
1398
|
-
/** The ticket, while it is open and `assignmentId` runs it. */
|
|
1399
1375
|
running(ticketId, assignmentId) {
|
|
1400
1376
|
const ticket = this.require(ticketId);
|
|
1401
1377
|
return liveTicket(ticket) && runningAssignment(ticket)?.id === assignmentId ? ticket : void 0;
|
|
1402
1378
|
}
|
|
1403
1379
|
};
|
|
1404
1380
|
// src/core/reasons.ts
|
|
1405
|
-
/**
|
|
1406
|
-
* Structured reason codes and templates for decisions, filtered triggers, and ended assignments.
|
|
1407
|
-
* Policy gates and source scopes may also emit their own literal reason codes.
|
|
1408
|
-
*/
|
|
1381
|
+
/** Policy gates and source scopes may also emit their own literal reason codes. */
|
|
1409
1382
|
const REASONS = {
|
|
1410
1383
|
said: "{text}",
|
|
1411
1384
|
timeout: "Assessment timed out",
|
|
@@ -1416,7 +1389,6 @@ const REASONS = {
|
|
|
1416
1389
|
routed: "Routed to ticket {ticket}",
|
|
1417
1390
|
explicitRouted: "Explicit request from an allowed sender routed by the orchestrator",
|
|
1418
1391
|
humanStop: "Human asked to stop",
|
|
1419
|
-
legacyImport: "Imported from the pre-domain-model task record",
|
|
1420
1392
|
forwarded: "Forwarded to the worker of the Ticket already on this Thread",
|
|
1421
1393
|
senderNotMember: "The sender is not on the Source's list",
|
|
1422
1394
|
retried: "Retried by a person",
|
|
@@ -1440,18 +1412,11 @@ function reasonText(reasons) {
|
|
|
1440
1412
|
}).join("; ");
|
|
1441
1413
|
}
|
|
1442
1414
|
// src/core/evals.ts
|
|
1443
|
-
/** Evals: the Thread-level assessments of queued Triggers, and the Decisions they end with. */
|
|
1444
|
-
/** An outcome's reason: a code it carries, else the words its assessor or person wrote. */
|
|
1445
1415
|
const reasonOf = (reason) => [typeof reason === "string" ? said(reason) : reason];
|
|
1446
1416
|
const stronger = (a, b) => a === "explicit" || b === "explicit" ? "explicit" : "inferred";
|
|
1447
1417
|
/** How many times in a row an expired Eval queues its Triggers again before it is recorded unavailable. */
|
|
1448
1418
|
const REQUEUES = 2;
|
|
1449
|
-
/**
|
|
1450
|
-
* Evals runs the Evals of Threads: it batches a Thread's queued Triggers into one Eval, takes its strength, actor, and basis
|
|
1451
|
-
* from the gate, asks the assessor, keeps its deadline through a pause, and records the Decision it ends with: accept, stop,
|
|
1452
|
-
* ignore, or unavailable. It depends on Tickets to create the ticket an accept decides and to end the one a stop names; the
|
|
1453
|
-
* Engine routes the main Session's decisions, people's replays, worker handbacks, and the pass's Eval work here.
|
|
1454
|
-
*/
|
|
1419
|
+
/** Depends on Tickets to create the ticket an accept decides and to end the one a stop names. */
|
|
1455
1420
|
var Evals = class extends EnginePart {
|
|
1456
1421
|
tickets;
|
|
1457
1422
|
replies;
|
|
@@ -1508,7 +1473,6 @@ var Evals = class extends EnginePart {
|
|
|
1508
1473
|
this.repo.threads.save(thread);
|
|
1509
1474
|
this.options.assessor.release?.(id);
|
|
1510
1475
|
}
|
|
1511
|
-
/** Start one Eval of the Triggers queued on a Thread, judged by the gate, and ask the assessor for it. */
|
|
1512
1476
|
start(threadKeyValue, policy) {
|
|
1513
1477
|
const thread = this.repo.threads.get(threadKeyValue);
|
|
1514
1478
|
const at = this.now();
|
|
@@ -1591,16 +1555,14 @@ var Evals = class extends EnginePart {
|
|
|
1591
1555
|
});
|
|
1592
1556
|
this.options.assessor.request(this.candidateOf(thread));
|
|
1593
1557
|
}
|
|
1594
|
-
/** The candidate of the Eval that holds `triggerId`, or undefined while no Eval holds it. */
|
|
1595
1558
|
candidate(triggerId) {
|
|
1596
1559
|
const thread = this.holding(triggerId);
|
|
1597
1560
|
return thread && this.candidateOf(thread);
|
|
1598
1561
|
}
|
|
1599
1562
|
/**
|
|
1600
|
-
*
|
|
1601
|
-
*
|
|
1602
|
-
*
|
|
1603
|
-
* mode or without enabled matching rules.
|
|
1563
|
+
* @throws UserError when the Trigger is unknown or not admissible, `outcome.evalId` names an Eval that has finished,
|
|
1564
|
+
* a ticket's worker still decides the Trigger and the outcome is not a stop, or an inferred candidate is accepted
|
|
1565
|
+
* outside `auto` mode or without enabled matching rules.
|
|
1604
1566
|
*/
|
|
1605
1567
|
decide(triggerId, outcome) {
|
|
1606
1568
|
const trigger = this.repo.triggers.get(triggerId);
|
|
@@ -1631,7 +1593,6 @@ var Evals = class extends EnginePart {
|
|
|
1631
1593
|
return this.decideEval(thread, evaluation, outcome);
|
|
1632
1594
|
}
|
|
1633
1595
|
/**
|
|
1634
|
-
* Queue a stored Trigger for a fresh Eval on its primary Thread and emit `trigger.requeued`; returns false while an Eval holds it.
|
|
1635
1596
|
* @throws UserError when the Trigger is unknown.
|
|
1636
1597
|
*/
|
|
1637
1598
|
reevaluate(triggerId) {
|
|
@@ -1639,7 +1600,6 @@ var Evals = class extends EnginePart {
|
|
|
1639
1600
|
this.emit("trigger.requeued", { triggerId });
|
|
1640
1601
|
return true;
|
|
1641
1602
|
}
|
|
1642
|
-
/** Queue a Trigger for a fresh Eval on its primary Thread; false while an Eval holds it. */
|
|
1643
1603
|
requeue(triggerId) {
|
|
1644
1604
|
const trigger = this.repo.triggers.get(triggerId);
|
|
1645
1605
|
if (!trigger) throw new UserError("unknownTrigger");
|
|
@@ -1656,10 +1616,10 @@ var Evals = class extends EnginePart {
|
|
|
1656
1616
|
return true;
|
|
1657
1617
|
}
|
|
1658
1618
|
/**
|
|
1659
|
-
* Record that a ticket's Executor hands back a Trigger that reached it as beyond the ticket, and queue it for a fresh
|
|
1660
|
-
* when the ticket decided it and no other ticket still does (see `handBack`): the gate judges it as any message
|
|
1661
|
-
* Thread, and its candidate carries the reasons, so no requeue notice comes before it. The ticket keeps its
|
|
1662
|
-
* and hands a Trigger back once.
|
|
1619
|
+
* Record that a ticket's Executor hands back a Trigger that reached it as beyond the ticket, and queue it for a fresh
|
|
1620
|
+
* Eval when the ticket decided it and no other ticket still does (see `handBack`): the gate judges it as any message
|
|
1621
|
+
* on its Thread, and its candidate carries the reasons, so no requeue notice comes before it. The ticket keeps its
|
|
1622
|
+
* Subscription, and hands a Trigger back once.
|
|
1663
1623
|
* @returns whether it was queued: false while another ticket decides it, and for one the ticket did not decide.
|
|
1664
1624
|
*/
|
|
1665
1625
|
handback(ticketId, triggerId, reason) {
|
|
@@ -1667,10 +1627,7 @@ var Evals = class extends EnginePart {
|
|
|
1667
1627
|
if (this.repo.triggers.disposition(triggerId)?.handbacks?.some((item) => item.ticketId === ticketId)) throw new UserError("handedBack");
|
|
1668
1628
|
return this.handBack(ticketId, this.repo.triggers.get(triggerId), reason);
|
|
1669
1629
|
}
|
|
1670
|
-
/**
|
|
1671
|
-
* Marks an `unavailable` Decision dismissed so the overview lists it no more.
|
|
1672
|
-
* @throws UserError when the Decision is unknown or not `unavailable`.
|
|
1673
|
-
*/
|
|
1630
|
+
/** @throws UserError when the Decision is unknown or not `unavailable`. */
|
|
1674
1631
|
dismiss(decisionId) {
|
|
1675
1632
|
const decision = this.repo.decisions.get(decisionId);
|
|
1676
1633
|
if (decision?.kind !== "unavailable") throw new UserError("unknownProblem");
|
|
@@ -1686,8 +1643,8 @@ var Evals = class extends EnginePart {
|
|
|
1686
1643
|
this.repo.threads.save(thread);
|
|
1687
1644
|
}
|
|
1688
1645
|
/**
|
|
1689
|
-
* End the Eval that holds a Trigger routed to a ticket, since the ticket has it now: one that holds other Triggers
|
|
1690
|
-
* queues them again for a fresh Eval without it.
|
|
1646
|
+
* End the Eval that holds a Trigger routed to a ticket, since the ticket has it now: one that holds other Triggers
|
|
1647
|
+
* too queues them again for a fresh Eval without it.
|
|
1691
1648
|
*/
|
|
1692
1649
|
routed(triggerId, ticketId) {
|
|
1693
1650
|
const held = this.holding(triggerId);
|
|
@@ -1704,7 +1661,6 @@ var Evals = class extends EnginePart {
|
|
|
1704
1661
|
triggerId
|
|
1705
1662
|
}, [triggerId]);
|
|
1706
1663
|
}
|
|
1707
|
-
/** Start the Evals in progress again under the new Policy version, and say that it changed. */
|
|
1708
1664
|
policyChanged(from, to) {
|
|
1709
1665
|
for (const thread of this.repo.threads.active()) if (thread.evaluating) this.reassess(thread, { code: "policyChanged" });
|
|
1710
1666
|
this.emit("policy.changed", {
|
|
@@ -1712,7 +1668,6 @@ var Evals = class extends EnginePart {
|
|
|
1712
1668
|
to
|
|
1713
1669
|
});
|
|
1714
1670
|
}
|
|
1715
|
-
/** End a Thread's Eval as unavailable for `reason` and queue its Triggers, except `without`, for a new Eval ahead of the others. */
|
|
1716
1671
|
reassess(thread, reason, without = []) {
|
|
1717
1672
|
const ids = thread.evaluating.triggerIds.filter((id) => !without.includes(id));
|
|
1718
1673
|
const decision = this.finish(thread, thread.evaluating, {
|
|
@@ -1747,8 +1702,8 @@ var Evals = class extends EnginePart {
|
|
|
1747
1702
|
};
|
|
1748
1703
|
}
|
|
1749
1704
|
/**
|
|
1750
|
-
* Record that the queued Triggers `ids` of a Thread start no Eval, for `reason`, and take them out of its queue. Its
|
|
1751
|
-
* differs from an Eval's, so it never replaces the Decision of an earlier Eval of the same Triggers.
|
|
1705
|
+
* Record that the queued Triggers `ids` of a Thread start no Eval, for `reason`, and take them out of its queue. Its
|
|
1706
|
+
* id differs from an Eval's, so it never replaces the Decision of an earlier Eval of the same Triggers.
|
|
1752
1707
|
*/
|
|
1753
1708
|
ignore(thread, ids, reason, policy, at) {
|
|
1754
1709
|
const evaluation = {
|
|
@@ -1773,8 +1728,9 @@ var Evals = class extends EnginePart {
|
|
|
1773
1728
|
delete thread.expired;
|
|
1774
1729
|
}
|
|
1775
1730
|
/**
|
|
1776
|
-
* The accept Decisions of the Triggers whose primary Thread is `threadKey`, by Trigger: what each one
|
|
1777
|
-
* its Decision run on its Triggers' primary Thread, so the Thread's Decisions, read
|
|
1731
|
+
* The accept Decisions of the Triggers whose primary Thread is `threadKey`, by Trigger: what each one
|
|
1732
|
+
* started. An Eval and its Decision run on its Triggers' primary Thread, so the Thread's Decisions, read
|
|
1733
|
+
* by their index, hold them all.
|
|
1778
1734
|
*/
|
|
1779
1735
|
accepted(threadKey) {
|
|
1780
1736
|
const accepted = /* @__PURE__ */ new Map();
|
|
@@ -1785,9 +1741,10 @@ var Evals = class extends EnginePart {
|
|
|
1785
1741
|
return accepted;
|
|
1786
1742
|
}
|
|
1787
1743
|
/**
|
|
1788
|
-
* A gate-only Eval of one Trigger, for an outcome decided without a queued Eval, such as on a follow-up
|
|
1789
|
-
* ticket. It is decided at once, beside an Eval its Thread may have in progress, and takes
|
|
1790
|
-
* Its id names the outcome's kind, so a stop and an accept of one message
|
|
1744
|
+
* A gate-only Eval of one Trigger, for an outcome decided without a queued Eval, such as on a follow-up
|
|
1745
|
+
* that reached a ticket. It is decided at once, beside an Eval its Thread may have in progress, and takes
|
|
1746
|
+
* the Trigger out of the queue. Its id names the outcome's kind, so a stop and an accept of one message
|
|
1747
|
+
* at the same moment are two Decisions.
|
|
1791
1748
|
*/
|
|
1792
1749
|
directEval(thread, trigger, kind, ticketId) {
|
|
1793
1750
|
const policy = this.policy();
|
|
@@ -1811,7 +1768,6 @@ var Evals = class extends EnginePart {
|
|
|
1811
1768
|
deadline: at
|
|
1812
1769
|
};
|
|
1813
1770
|
}
|
|
1814
|
-
/** Apply an outcome to an Eval of a Thread: the one in progress, or a gate-only one. */
|
|
1815
1771
|
decideEval(thread, evaluation, outcome) {
|
|
1816
1772
|
const audit = outcome.audit ? { audit: outcome.audit } : {};
|
|
1817
1773
|
if (this.policy().version !== evaluation.policyVersion) {
|
|
@@ -1855,14 +1811,16 @@ var Evals = class extends EnginePart {
|
|
|
1855
1811
|
}
|
|
1856
1812
|
return this.accept(thread, evaluation, outcome);
|
|
1857
1813
|
}
|
|
1858
|
-
/**
|
|
1814
|
+
/**
|
|
1815
|
+
* An explicit request a person on the list addressed to the agent, which the assessor ignored in its own words, gets
|
|
1816
|
+
* one refusal where it was written, while the Policy allows refusals.
|
|
1817
|
+
*/
|
|
1859
1818
|
refuse(evaluation, reason) {
|
|
1860
1819
|
if (evaluation.strength !== "explicit" || evaluation.basis.via !== "addressed" || !this.policy().refuses) return;
|
|
1861
1820
|
const trigger = evaluation.primary ? this.repo.triggers.get(evaluation.primary) : void 0;
|
|
1862
1821
|
if (!trigger?.human || !trigger.addressed || !trigger.channel) return;
|
|
1863
1822
|
this.replies.refuse(trigger, reason);
|
|
1864
1823
|
}
|
|
1865
|
-
/** Record an accept with the Mandate the Eval gives, and have Tickets create its ticket. */
|
|
1866
1824
|
accept(thread, evaluation, outcome) {
|
|
1867
1825
|
const at = this.now();
|
|
1868
1826
|
const basis = {
|
|
@@ -1889,7 +1847,6 @@ var Evals = class extends EnginePart {
|
|
|
1889
1847
|
this.tickets.create(ticketId, decision.id, mandate, evaluation, outcome, at);
|
|
1890
1848
|
return decision;
|
|
1891
1849
|
}
|
|
1892
|
-
/** Record an Eval's Decision; the Thread's own Eval ends with it, while a gate-only one leaves the Thread's Eval going. */
|
|
1893
1850
|
finish(thread, evaluation, outcome) {
|
|
1894
1851
|
const decision = this.record(thread, evaluation, outcome);
|
|
1895
1852
|
if (thread.evaluating?.id !== evaluation.id) return decision;
|
|
@@ -1936,12 +1893,8 @@ var Evals = class extends EnginePart {
|
|
|
1936
1893
|
}
|
|
1937
1894
|
};
|
|
1938
1895
|
// src/core/intake.ts
|
|
1939
|
-
/** Trigger ingestion, thread correlation, subscription fan-out, and evaluation queuing. */
|
|
1940
1896
|
/**
|
|
1941
|
-
*
|
|
1942
|
-
* Fans listened triggers out to subscribed tickets via Tickets and queues for Eval, on its primary thread, every trigger no
|
|
1943
|
-
* ticket's worker decides (see `followupOwners`).
|
|
1944
|
-
* Engine delegates trigger reception to this part.
|
|
1897
|
+
* Queues on its primary thread every trigger that no ticket's worker decides (see `followupOwners`).
|
|
1945
1898
|
*/
|
|
1946
1899
|
var Intake = class extends EnginePart {
|
|
1947
1900
|
tickets;
|
|
@@ -2011,7 +1964,6 @@ var Intake = class extends EnginePart {
|
|
|
2011
1964
|
}
|
|
2012
1965
|
};
|
|
2013
1966
|
// src/core/replies.ts
|
|
2014
|
-
/** The Engine's messages to Channels, each by the rule that allows it. */
|
|
2015
1967
|
const REFUSAL_PREFIX = "I can't take this on here: ";
|
|
2016
1968
|
const REFUSAL_FALLBACK = "it is outside what I can do.";
|
|
2017
1969
|
const INTERNAL = /\b(?:tickets?|workers?|hand(?:ed|s|ing)?[\s-]?backs?|evals?|triggers?|orchestrator|assignments?)\b|\b[0-9a-f]{16,}\b/i;
|
|
@@ -2026,7 +1978,11 @@ function readable$1(sentence) {
|
|
|
2026
1978
|
const joined = kept.join(", ").replace(/[\s.!?。!?]+$/u, "");
|
|
2027
1979
|
return joined.charAt(0).toUpperCase() + joined.slice(1) + ".";
|
|
2028
1980
|
}
|
|
2029
|
-
/**
|
|
1981
|
+
/**
|
|
1982
|
+
* The refusal a person gets for an explicit request the assessor ignored: the first sentence of its reason that a
|
|
1983
|
+
* person can read: internal terms rewritten (`the trigger` → `the message`) or their clauses dropped; a fixed phrase
|
|
1984
|
+
* when nothing is left; at most 200 characters.
|
|
1985
|
+
*/
|
|
2030
1986
|
function refusalText(reason) {
|
|
2031
1987
|
let body = REFUSAL_FALLBACK;
|
|
2032
1988
|
for (const sentence of reason.trim().split(SENTENCE)) {
|
|
@@ -2039,33 +1995,24 @@ function refusalText(reason) {
|
|
|
2039
1995
|
const text = REFUSAL_PREFIX + body;
|
|
2040
1996
|
return text.length <= 200 ? text : text.slice(0, 199).trimEnd() + "…";
|
|
2041
1997
|
}
|
|
2042
|
-
/** The note a person gets once a turn or budget limit stopped the work: which limit, the work so far when there is a pull request, and an invitation to reply (ADR-32). */
|
|
2043
1998
|
function limitText(limit, pull) {
|
|
2044
1999
|
return `I stopped before finishing: the ${limit} limit for this task was reached.` + (pull ? ` Work so far: ${pull}.` : "") + " Reply here to continue.";
|
|
2045
2000
|
}
|
|
2046
2001
|
/**
|
|
2047
|
-
*
|
|
2048
|
-
*
|
|
2049
|
-
* that keeps it from going out twice (idempotent). The Engine routes `reply` here; Tickets asks it for the acknowledgement
|
|
2050
|
-
* an accept carries, and Assignments asks it to withdraw a question a report replaced.
|
|
2002
|
+
* Each message is queued under a deterministic id, so it is never sent twice. Replies to a person go where the person's
|
|
2003
|
+
* message was written, and a result where the latest person's message to the ticket was written.
|
|
2051
2004
|
*/
|
|
2052
2005
|
var Replies = class extends EnginePart {
|
|
2053
2006
|
/**
|
|
2054
|
-
*
|
|
2055
|
-
*
|
|
2056
|
-
*
|
|
2057
|
-
*
|
|
2058
|
-
*
|
|
2059
|
-
* @throws UserError when the ticket or trigger is unknown or the purpose's rule refuses it, such as a result of a ticket
|
|
2060
|
-
* that is not ready, completed, merged, or dropped.
|
|
2007
|
+
* An acknowledgement or an answer goes where that person's message was written; attention where the message that
|
|
2008
|
+
* raised the question was, else to the ticket's starting conversation. Each message is queued at most once
|
|
2009
|
+
* (idempotent). An acknowledgement, even one already queued, records that the ticket's worker took the message up.
|
|
2010
|
+
* @throws UserError when the ticket or trigger is unknown or the purpose's rule refuses it, such as a result of a
|
|
2011
|
+
* ticket that is not ready, completed, merged, or dropped.
|
|
2061
2012
|
*/
|
|
2062
2013
|
reply(request) {
|
|
2063
2014
|
return request.purpose === "acknowledgement" ? this.acknowledge(request) : this.answer(request);
|
|
2064
2015
|
}
|
|
2065
|
-
/**
|
|
2066
|
-
* Queue the acknowledgement an accept carries where the message that started the ticket was written, if it has a Channel
|
|
2067
|
-
* and a person wrote it.
|
|
2068
|
-
*/
|
|
2069
2016
|
acknowledgeAccepted(ticketId, trigger, text) {
|
|
2070
2017
|
if (trigger.channel && trigger.human) this.options.outbox.send({
|
|
2071
2018
|
id: key("ack", trigger.id),
|
|
@@ -2075,7 +2022,10 @@ var Replies = class extends EnginePart {
|
|
|
2075
2022
|
ticketId
|
|
2076
2023
|
});
|
|
2077
2024
|
}
|
|
2078
|
-
/**
|
|
2025
|
+
/**
|
|
2026
|
+
* Queue, once per Assignment, the note that a turn or budget limit stopped the work, where the latest person's
|
|
2027
|
+
* message to the ticket was written, else where it started; id `limit:<assignmentId>`.
|
|
2028
|
+
*/
|
|
2079
2029
|
limitStopped(ticket, assignmentId, limit, pull) {
|
|
2080
2030
|
const channel = this.replyChannel(ticket);
|
|
2081
2031
|
if (channel) this.options.outbox.send({
|
|
@@ -2086,11 +2036,13 @@ var Replies = class extends EnginePart {
|
|
|
2086
2036
|
ticketId: ticket.id
|
|
2087
2037
|
});
|
|
2088
2038
|
}
|
|
2089
|
-
/** Withdraw a ticket's question for a person from the outbox, unless it went out already. */
|
|
2090
2039
|
withdrawAttention(attentionId) {
|
|
2091
2040
|
this.options.outbox.withdraw(key("attention-reply", attentionId));
|
|
2092
2041
|
}
|
|
2093
|
-
/**
|
|
2042
|
+
/**
|
|
2043
|
+
* Queue the refusal of a person's explicit request once, where it was written: its id is `refusal:<triggerId>`, so a
|
|
2044
|
+
* replay or restart does not send it again.
|
|
2045
|
+
*/
|
|
2094
2046
|
refuse(trigger, reason) {
|
|
2095
2047
|
if (!trigger.channel) return;
|
|
2096
2048
|
this.options.outbox.send({
|
|
@@ -2164,7 +2116,10 @@ var Replies = class extends EnginePart {
|
|
|
2164
2116
|
sourceChannel(ticket) {
|
|
2165
2117
|
return this.source(ticket)?.channel ?? ticket.channels[0];
|
|
2166
2118
|
}
|
|
2167
|
-
/**
|
|
2119
|
+
/**
|
|
2120
|
+
* Where a ticket's result goes: the Channel of the latest person's message that reached it on a Thread it works on
|
|
2121
|
+
* (not one it follows for context), else the conversation it started in.
|
|
2122
|
+
*/
|
|
2168
2123
|
replyChannel(ticket) {
|
|
2169
2124
|
const latest = ticket.inputs.findLast((input) => input.kind === "trigger" && input.human === true && !input.context && this.repo.triggers.get(input.triggerId)?.channel !== void 0);
|
|
2170
2125
|
return (latest && this.repo.triggers.get(latest.triggerId)?.channel) ?? this.sourceChannel(ticket);
|
|
@@ -2173,9 +2128,8 @@ var Replies = class extends EnginePart {
|
|
|
2173
2128
|
// src/core/engine.ts
|
|
2174
2129
|
/** The domain Engine: the single entry point of the core, and the role interfaces its callers receive. */
|
|
2175
2130
|
/**
|
|
2176
|
-
*
|
|
2177
|
-
*
|
|
2178
|
-
* effects through ports. Callers receive role-scoped `Pick` interfaces rather than the full engine.
|
|
2131
|
+
* Mutations run in synchronous repository transactions and queue side effects through ports. Callers receive
|
|
2132
|
+
* role-scoped `Pick` interfaces rather than the full engine.
|
|
2179
2133
|
*/
|
|
2180
2134
|
var Engine = class {
|
|
2181
2135
|
repo;
|
|
@@ -2195,26 +2149,27 @@ var Engine = class {
|
|
|
2195
2149
|
this.intake = new Intake(options, this.tickets);
|
|
2196
2150
|
}
|
|
2197
2151
|
/**
|
|
2198
|
-
* A Source's request to take in a Trigger: store it once, fan it out to the tickets that subscribe to its Threads (to
|
|
2199
|
-
* ended one only a person's message, which resumes it at once unless processing is paused, and to none a person
|
|
2200
|
-
* and queue it for Eval on its primary Thread unless a ticket's worker decides it (see `followupOwners`).
|
|
2152
|
+
* A Source's request to take in a Trigger: store it once, fan it out to the tickets that subscribe to its Threads (to
|
|
2153
|
+
* an ended one only a person's message, which resumes it at once unless processing is paused, and to none a person
|
|
2154
|
+
* stopped), and queue it for Eval on its primary Thread unless a ticket's worker decides it (see `followupOwners`).
|
|
2201
2155
|
* Triggers that arrive while an Eval of that Thread runs wait together for the next one.
|
|
2202
2156
|
*/
|
|
2203
2157
|
receive(trigger) {
|
|
2204
2158
|
return this.repo.atomic(() => this.intake.receive(trigger));
|
|
2205
2159
|
}
|
|
2206
2160
|
/**
|
|
2207
|
-
* A Source's request to make an open ticket follow one of its Threads, such as a pull request's, `own` when the
|
|
2208
|
-
* opened it: the ticket subscribes to it, and a ready report ends it only once `followsDone` holds (see
|
|
2161
|
+
* A Source's request to make an open ticket follow one of its Threads, such as a pull request's, `own` when the
|
|
2162
|
+
* ticket opened it: the ticket subscribes to it, and a ready report ends it only once `followsDone` holds (see
|
|
2163
|
+
* `Tickets.followsDone`).
|
|
2209
2164
|
* @throws UserError when the ticket is unknown or has ended.
|
|
2210
2165
|
*/
|
|
2211
2166
|
bindThread(ticketId, thread, own, url) {
|
|
2212
2167
|
return this.repo.atomic(() => this.tickets.bind(ticketId, thread, own, url));
|
|
2213
2168
|
}
|
|
2214
2169
|
/**
|
|
2215
|
-
* A Source's fact that a Thread closed, such as its pull request's merge, at the time it happened. An open ticket
|
|
2216
|
-
* follows it ends once it is done with the Threads it follows, unless work resumed since the close has not
|
|
2217
|
-
* its worker took up a person's message the ticket decides after its latest report.
|
|
2170
|
+
* A Source's fact that a Thread closed, such as its pull request's merge, at the time it happened. An open ticket
|
|
2171
|
+
* that follows it ends once it is done with the Threads it follows, unless work resumed since the close has not
|
|
2172
|
+
* finished, or its worker took up a person's message the ticket decides after its latest report.
|
|
2218
2173
|
*/
|
|
2219
2174
|
threadClosed(thread, outcome, at) {
|
|
2220
2175
|
this.repo.atomic(() => {
|
|
@@ -2222,10 +2177,11 @@ var Engine = class {
|
|
|
2222
2177
|
});
|
|
2223
2178
|
}
|
|
2224
2179
|
/**
|
|
2225
|
-
* Requeues overdue Evals up to `REQUEUES` times before recording `unavailable` (undelivered Evals never expire),
|
|
2226
|
-
* Evals of Threads whose queued Triggers wait, and resumes ended tickets a person's message reached while
|
|
2227
|
-
* paused. While processing is paused, nothing expires: each Eval in progress notes when the pause
|
|
2228
|
-
* moves forward by the pause's length once processing resumes. Each Eval and each ticket
|
|
2180
|
+
* Requeues overdue Evals up to `REQUEUES` times before recording `unavailable` (undelivered Evals never expire),
|
|
2181
|
+
* starts the Evals of Threads whose queued Triggers wait, and resumes ended tickets a person's message reached while
|
|
2182
|
+
* processing was paused. While processing is paused, nothing expires: each Eval in progress notes when the pause
|
|
2183
|
+
* began, and its deadline moves forward by the pause's length once processing resumes. Each Eval and each ticket
|
|
2184
|
+
* update takes one transaction.
|
|
2229
2185
|
*/
|
|
2230
2186
|
evaluateDue() {
|
|
2231
2187
|
const policy = this.options.policy();
|
|
@@ -2246,23 +2202,23 @@ var Engine = class {
|
|
|
2246
2202
|
}
|
|
2247
2203
|
/**
|
|
2248
2204
|
* Decide a Trigger: the Eval that holds it, or a direct Eval of it alone when none does, such as a person's explicit
|
|
2249
|
-
* request the main Session routes. A proactive candidate is accepted only in `auto` mode with enabled rules its
|
|
2250
|
-
* matched, and a Trigger that already started a ticket returns that ticket's Decision. An invalid stop is
|
|
2251
|
-
* `ignore`; one judged under an older Policy as `unavailable`.
|
|
2252
|
-
* @throws UserError when the Trigger is unknown or not admissible, `outcome.evalId` names an Eval that has finished,
|
|
2253
|
-
* ticket's worker still decides the Trigger and the outcome is not a stop, or an inferred candidate is accepted
|
|
2254
|
-
* mode or without enabled matching rules.
|
|
2205
|
+
* request the main Session routes. A proactive candidate is accepted only in `auto` mode with enabled rules its
|
|
2206
|
+
* assessment matched, and a Trigger that already started a ticket returns that ticket's Decision. An invalid stop is
|
|
2207
|
+
* recorded as `ignore`; one judged under an older Policy as `unavailable`.
|
|
2208
|
+
* @throws UserError when the Trigger is unknown or not admissible, `outcome.evalId` names an Eval that has finished,
|
|
2209
|
+
* a ticket's worker still decides the Trigger and the outcome is not a stop, or an inferred candidate is accepted
|
|
2210
|
+
* outside `auto` mode or without enabled matching rules.
|
|
2255
2211
|
*/
|
|
2256
2212
|
decide(triggerId, outcome) {
|
|
2257
2213
|
return this.repo.atomic(() => this.evals.decide(triggerId, outcome));
|
|
2258
2214
|
}
|
|
2259
2215
|
/**
|
|
2260
|
-
* Attach a Trigger to a ticket that the message points at: the ticket follows the Trigger's Threads and receives it
|
|
2261
|
-
* Input with the routing note, and the Eval that holds the Trigger ends, since the ticket has it now; the other
|
|
2262
|
-
* its batch wait for a fresh Eval. A note for an Input the worker already received goes out as a new
|
|
2263
|
-
* message the ticket let go, read for context only, or had before it ended. An ended ticket
|
|
2264
|
-
* once unless processing is paused.
|
|
2265
|
-
*
|
|
2216
|
+
* Attach a Trigger to a ticket that the message points at: the ticket follows the Trigger's Threads and receives it
|
|
2217
|
+
* as an Input with the routing note, and the Eval that holds the Trigger ends, since the ticket has it now; the other
|
|
2218
|
+
* Triggers of its batch wait for a fresh Eval. A note for an Input the worker already received goes out as a new
|
|
2219
|
+
* Input, and so does a message the ticket let go, read for context only, or had before it ended. An ended ticket
|
|
2220
|
+
* resumes under its Mandate, at once unless processing is paused. @throws UserError for a ticket a person stopped,
|
|
2221
|
+
* which withdrew its Mandate, or one whose Mandate the Policy no longer holds.
|
|
2266
2222
|
*/
|
|
2267
2223
|
attach(ticketId, triggerId, note) {
|
|
2268
2224
|
return this.repo.atomic(() => {
|
|
@@ -2284,11 +2240,11 @@ var Engine = class {
|
|
|
2284
2240
|
return this.repo.atomic(() => this.tickets.requestStatus(ticketId));
|
|
2285
2241
|
}
|
|
2286
2242
|
/**
|
|
2287
|
-
* Queue a message for the Channel its purpose names: an acknowledgement or an answer goes where the person's message
|
|
2288
|
-
* written, attention where the message that raised the question was, else, like a result, to the conversation the
|
|
2289
|
-
* started in. A message queued before is not queued again (see `Replies.reply`).
|
|
2290
|
-
* @throws UserError when the ticket or trigger is unknown or the purpose's rule refuses it, such as a result of a
|
|
2291
|
-
* is not ready, completed, merged, or dropped.
|
|
2243
|
+
* Queue a message for the Channel its purpose names: an acknowledgement or an answer goes where the person's message
|
|
2244
|
+
* was written, attention where the message that raised the question was, else, like a result, to the conversation the
|
|
2245
|
+
* ticket started in. A message queued before is not queued again (see `Replies.reply`).
|
|
2246
|
+
* @throws UserError when the ticket or trigger is unknown or the purpose's rule refuses it, such as a result of a
|
|
2247
|
+
* ticket that is not ready, completed, merged, or dropped.
|
|
2292
2248
|
*/
|
|
2293
2249
|
reply(request) {
|
|
2294
2250
|
return this.repo.atomic(() => this.replies.reply(request));
|
|
@@ -2345,8 +2301,9 @@ var Engine = class {
|
|
|
2345
2301
|
});
|
|
2346
2302
|
}
|
|
2347
2303
|
/**
|
|
2348
|
-
* The Executor's fact that Inputs reached the Assignment that runs the ticket, which clears a failed
|
|
2349
|
-
* an Assignment that no longer runs is dropped. Its worker reads each once its
|
|
2304
|
+
* The Executor's fact that Inputs reached the Assignment that runs the ticket, which clears a failed
|
|
2305
|
+
* delivery. A fact about an Assignment that no longer runs is dropped. Its worker reads each once its
|
|
2306
|
+
* Session takes it into a step (`inputsRead`).
|
|
2350
2307
|
*/
|
|
2351
2308
|
inputsDelivered(ticketId, assignmentId, inputIds) {
|
|
2352
2309
|
this.repo.atomic(() => {
|
|
@@ -2354,8 +2311,8 @@ var Engine = class {
|
|
|
2354
2311
|
});
|
|
2355
2312
|
}
|
|
2356
2313
|
/**
|
|
2357
|
-
* The Executor's fact that the worker's Session of an open ticket took the messages that carried Inputs into a step,
|
|
2358
|
-
* which its model reads them: a report or the end of a turn after that counts them as read at the ticket's end.
|
|
2314
|
+
* The Executor's fact that the worker's Session of an open ticket took the messages that carried Inputs into a step,
|
|
2315
|
+
* from which its model reads them: a report or the end of a turn after that counts them as read at the ticket's end.
|
|
2359
2316
|
* @throws UserError when the ticket is unknown.
|
|
2360
2317
|
*/
|
|
2361
2318
|
inputsRead(ticketId, inputIds) {
|
|
@@ -2369,17 +2326,20 @@ var Engine = class {
|
|
|
2369
2326
|
this.assignments.turnEnded(ticketId, assignmentId);
|
|
2370
2327
|
});
|
|
2371
2328
|
}
|
|
2372
|
-
/**
|
|
2329
|
+
/**
|
|
2330
|
+
* The Executor's fact that delivering Inputs to the Assignment that runs the ticket failed; the ticket shows it until
|
|
2331
|
+
* a delivery succeeds.
|
|
2332
|
+
*/
|
|
2373
2333
|
deliveryFailed(ticketId, assignmentId, error) {
|
|
2374
2334
|
this.repo.atomic(() => {
|
|
2375
2335
|
this.assignments.deliveryFailed(ticketId, assignmentId, error);
|
|
2376
2336
|
});
|
|
2377
2337
|
}
|
|
2378
2338
|
/**
|
|
2379
|
-
* A worker's request, through its Executor, to mark a Thread attached to its ticket unrelated, for `reason`: only
|
|
2380
|
-
* ticket's Subscription drops, until a new Decision subscribes it again.
|
|
2381
|
-
* @throws UserError when the ticket is unknown, or for the Thread the ticket started on, one a Source bound it to
|
|
2382
|
-
* its pull request's), or one it does not follow.
|
|
2339
|
+
* A worker's request, through its Executor, to mark a Thread attached to its ticket unrelated, for `reason`: only
|
|
2340
|
+
* that ticket's Subscription drops, until a new Decision subscribes it again.
|
|
2341
|
+
* @throws UserError when the ticket is unknown, or for the Thread the ticket started on, one a Source bound it to
|
|
2342
|
+
* (such as its pull request's), or one it does not follow.
|
|
2383
2343
|
*/
|
|
2384
2344
|
markUnrelated(ticketId, thread, reason) {
|
|
2385
2345
|
this.repo.atomic(() => {
|
|
@@ -2387,7 +2347,8 @@ var Engine = class {
|
|
|
2387
2347
|
});
|
|
2388
2348
|
}
|
|
2389
2349
|
/**
|
|
2390
|
-
* An Executor's Subscription to a Thread it found related, for context: see `Tickets.followForContext` and
|
|
2350
|
+
* An Executor's Subscription to a Thread it found related, for context: see `Tickets.followForContext` and
|
|
2351
|
+
* `Thread.context`.
|
|
2391
2352
|
* @returns false when the ticket already follows the Thread.
|
|
2392
2353
|
* @throws UserError when the ticket is unknown or has ended.
|
|
2393
2354
|
*/
|
|
@@ -2403,32 +2364,37 @@ var Engine = class {
|
|
|
2403
2364
|
}
|
|
2404
2365
|
/**
|
|
2405
2366
|
* An Executor's hand-back of a message that reached its ticket but asks for more than it: see `Evals.handback`.
|
|
2406
|
-
* @returns whether it was queued for Eval: false while another ticket still decides it, and for one this ticket did
|
|
2407
|
-
*
|
|
2367
|
+
* @returns whether it was queued for Eval: false while another ticket still decides it, and for one this ticket did
|
|
2368
|
+
* not decide (see `Evals.handback`).
|
|
2369
|
+
* @throws UserError when the ticket or trigger is unknown, the trigger did not reach the ticket, or the ticket
|
|
2370
|
+
* already handed it back.
|
|
2408
2371
|
*/
|
|
2409
2372
|
handback(ticketId, triggerId, reason) {
|
|
2410
2373
|
return this.repo.atomic(() => this.evals.handback(ticketId, triggerId, reason));
|
|
2411
2374
|
}
|
|
2412
2375
|
/**
|
|
2413
2376
|
* Record a report on an open ticket's current Assignment. Its question for a person (`attention`) is asked, kept, or
|
|
2414
|
-
* cleared (`null` or a `ready` report), and a replaced attention question whose reply has not been sent yet is
|
|
2415
|
-
* from the outbox. A `blocked` or `failed` report holds back automated inputs until a human message
|
|
2416
|
-
* report ends the ticket, and a `ready` report ends it: `completed` without followed Threads,
|
|
2417
|
-
* outcome once `followsDone` holds.
|
|
2418
|
-
* @throws UserError for an unknown or ended ticket, an ended assignment, a question that comes with a ready result or
|
|
2419
|
-
* answer, or a question or answer that names a message that is not a person's or did not reach the ticket.
|
|
2377
|
+
* cleared (`null` or a `ready` report), and a replaced attention question whose reply has not been sent yet is
|
|
2378
|
+
* withdrawn from the outbox. A `blocked` or `failed` report holds back automated inputs until a human message
|
|
2379
|
+
* arrives. A `dropped` report ends the ticket, and a `ready` report ends it: `completed` without followed Threads,
|
|
2380
|
+
* else with the `followsDone` outcome once `followsDone` holds.
|
|
2381
|
+
* @throws UserError for an unknown or ended ticket, an ended assignment, a question that comes with a ready result or
|
|
2382
|
+
* an answer, or a question or answer that names a message that is not a person's or did not reach the ticket.
|
|
2420
2383
|
*/
|
|
2421
2384
|
report(ticketId, report) {
|
|
2422
2385
|
return this.repo.atomic(() => this.assignments.report(ticketId, report));
|
|
2423
2386
|
}
|
|
2424
|
-
/**
|
|
2387
|
+
/**
|
|
2388
|
+
* A worker's Session stopped at a turn or budget limit: see `Assignments.limited`.
|
|
2389
|
+
* @returns whether the ticket kept its ready report, so no blocked report follows.
|
|
2390
|
+
*/
|
|
2425
2391
|
limitReached(ticketId, assignmentId, limit) {
|
|
2426
2392
|
return this.repo.atomic(() => this.assignments.limited(ticketId, assignmentId, limit));
|
|
2427
2393
|
}
|
|
2428
2394
|
/**
|
|
2429
|
-
* Handles an assignment that was running when the host stopped: restarts it with a recovery note unless its latest
|
|
2430
|
-
* `waiting`, `ready`, `blocked`, or `failed` and no Input reached its worker after that report that a later
|
|
2431
|
-
* not take in
|
|
2395
|
+
* Handles an assignment that was running when the host stopped: restarts it with a recovery note unless its latest
|
|
2396
|
+
* report is `waiting`, `ready`, `blocked`, or `failed` and no Input reached its worker after that report that a later
|
|
2397
|
+
* finished turn did not take in, in which case it keeps its Session and state and gets the Inputs that wait for it.
|
|
2432
2398
|
*/
|
|
2433
2399
|
assignmentInterrupted(ticketId, assignmentId) {
|
|
2434
2400
|
this.repo.atomic(() => {
|
|
@@ -2436,8 +2402,8 @@ var Engine = class {
|
|
|
2436
2402
|
});
|
|
2437
2403
|
}
|
|
2438
2404
|
/**
|
|
2439
|
-
* The Policy changed: each open ticket in `effects` is told how the change bears on it, and Evals in progress start
|
|
2440
|
-
* under the new version.
|
|
2405
|
+
* The Policy changed: each open ticket in `effects` is told how the change bears on it, and Evals in progress start
|
|
2406
|
+
* again under the new version.
|
|
2441
2407
|
*/
|
|
2442
2408
|
policyChanged(from, to, effects = /* @__PURE__ */ new Map()) {
|
|
2443
2409
|
this.repo.atomic(() => {
|
|
@@ -2447,16 +2413,11 @@ var Engine = class {
|
|
|
2447
2413
|
}
|
|
2448
2414
|
};
|
|
2449
2415
|
// src/policy/actors.ts
|
|
2450
|
-
/** Compare two logins or repository names without case; no other normalisation
|
|
2416
|
+
/** Compare two logins or repository names without case; no other normalisation. */
|
|
2451
2417
|
const sameName = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
2452
2418
|
const BOT_SUFFIX = /\[bot\]$/i;
|
|
2453
|
-
/** A bot: the Source marked the Trigger as not written by a person (an account type other than a user, CI, or this agent), or the login ends in `[bot]`. */
|
|
2454
2419
|
const isBot = (trigger) => !trigger.human || BOT_SUFFIX.test(trigger.actor.id);
|
|
2455
|
-
/**
|
|
2456
|
-
* Judge a Trigger's actor and repository, in order: a repository outside `repositories` (or none) is `rejected-repo`; an empty
|
|
2457
|
-
* actor id (a CI transition) is a `source-fact`; an actor not on `allowed` is `rejected-bot` or `rejected-actor`; anyone else,
|
|
2458
|
-
* a bot named on the list included, is `allowed` (the gate still treats such a bot as inferred).
|
|
2459
|
-
*/
|
|
2420
|
+
/** A bot named on the list is `allowed` here; the gate still admits it only as an inferred candidate. */
|
|
2460
2421
|
function actorVerdict(trigger, rule) {
|
|
2461
2422
|
const repository = trigger.meta.repository;
|
|
2462
2423
|
if (typeof repository !== "string" || !rule.repositories.some((name) => sameName(name, repository))) return "rejected-repo";
|
|
@@ -2466,8 +2427,8 @@ function actorVerdict(trigger, rule) {
|
|
|
2466
2427
|
}
|
|
2467
2428
|
// src/policy/policy.ts
|
|
2468
2429
|
/**
|
|
2469
|
-
*
|
|
2470
|
-
*
|
|
2430
|
+
* Who may originate work is one global list of logins, compared without case; a bot on it is an inferred
|
|
2431
|
+
* candidate at most.
|
|
2471
2432
|
*/
|
|
2472
2433
|
const RuleIdSchema = z.string().regex(/^[a-zA-Z0-9_-]{1,80}$/);
|
|
2473
2434
|
const RuleAssessmentSchema = z.object({
|
|
@@ -2484,19 +2445,16 @@ const AdmissionDirectionSchema = z.object({
|
|
|
2484
2445
|
requires_design_decision: z.boolean(),
|
|
2485
2446
|
evidence: z.string().trim().min(1).max(700)
|
|
2486
2447
|
}).strict();
|
|
2487
|
-
/** Check whether an assessment established implementation intent and a settled target without open design decisions. */
|
|
2488
2448
|
function directionAllowed(value) {
|
|
2489
2449
|
const result = AdmissionDirectionSchema.safeParse(value);
|
|
2490
2450
|
return result.success && result.data.implementation_requested && result.data.target_basis !== "undetermined" && !result.data.requires_design_decision;
|
|
2491
2451
|
}
|
|
2492
2452
|
/**
|
|
2493
|
-
*
|
|
2494
|
-
*
|
|
2495
|
-
* `observe` records `would_accept`, and `off` ignores and records nothing, for explicit and inferred candidates alike.
|
|
2496
|
-
* The `strength` argument is kept for caller compatibility and no longer changes the result.
|
|
2453
|
+
* Accepts only in `auto` mode; `observe` records `would_accept`, and `off` ignores and records nothing, for explicit
|
|
2454
|
+
* and inferred candidates alike.
|
|
2497
2455
|
* @throws when `rule_assessments` does not cover every enabled rule exactly once, or an accept omits `intent_evidence`.
|
|
2498
2456
|
*/
|
|
2499
|
-
function assessmentOutcome(assessment, settings
|
|
2457
|
+
function assessmentOutcome(assessment, settings) {
|
|
2500
2458
|
const enabled = settings.policy.rules.filter((rule) => rule.enabled);
|
|
2501
2459
|
const judged = assessment.rule_assessments;
|
|
2502
2460
|
const ids = new Set(judged.map((item) => item.rule_id));
|
|
@@ -2617,8 +2575,8 @@ function canonical(value) {
|
|
|
2617
2575
|
return JSON.stringify(value, (_key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : item);
|
|
2618
2576
|
}
|
|
2619
2577
|
/**
|
|
2620
|
-
*
|
|
2621
|
-
*
|
|
2578
|
+
* Hashes only the settings that affect work authorization; model, concurrency, pause and polling changes
|
|
2579
|
+
* keep decisions valid.
|
|
2622
2580
|
*/
|
|
2623
2581
|
function policyVersion(settings) {
|
|
2624
2582
|
const { rules, acceptMode, allowed, admission } = settings.policy;
|
|
@@ -2629,11 +2587,10 @@ function policyVersion(settings) {
|
|
|
2629
2587
|
admission
|
|
2630
2588
|
}));
|
|
2631
2589
|
}
|
|
2632
|
-
/**
|
|
2590
|
+
/** Compared without case. The list is global, so `actor.source` is not consulted. */
|
|
2633
2591
|
function allowedSender(settings, actor) {
|
|
2634
2592
|
return settings.policy.allowed.find((entry) => sameName(entry, actor.id));
|
|
2635
2593
|
}
|
|
2636
|
-
/** Check whether a thread has tickets of its own (excluding context-only subscriptions). */
|
|
2637
2594
|
const participating = (thread, tickets) => [...thread.subscriptions, ...tickets.map((ticket) => ticket.id)].some((id) => !thread.context?.includes(id));
|
|
2638
2595
|
const listened = (sources, source, meta) => sources.scope({
|
|
2639
2596
|
source,
|
|
@@ -2650,7 +2607,6 @@ const STRENGTH = {
|
|
|
2650
2607
|
inferred: 1,
|
|
2651
2608
|
explicit: 2
|
|
2652
2609
|
};
|
|
2653
|
-
/** Compute how a settings change from `old` to `now` affects the authorization or matched rules of each open ticket. */
|
|
2654
2610
|
function policyEffects(tickets, old, now, scope) {
|
|
2655
2611
|
const before = settingsPolicy(old, scope(old)), after = settingsPolicy(now, scope(now));
|
|
2656
2612
|
const effects = /* @__PURE__ */ new Map();
|
|
@@ -2688,11 +2644,10 @@ function policyEffects(tickets, old, now, scope) {
|
|
|
2688
2644
|
}
|
|
2689
2645
|
return effects;
|
|
2690
2646
|
}
|
|
2691
|
-
/** Build the active Policy for gating incoming Triggers and revalidating existing Ticket Mandates. */
|
|
2692
2647
|
function settingsPolicy(settings, sources) {
|
|
2693
2648
|
const version = policyVersion(settings);
|
|
2694
2649
|
const mode = settings.policy.acceptMode;
|
|
2695
|
-
/**
|
|
2650
|
+
/** Only listed bots (as inferred candidates) and senderless system/CI events pass; other bots are rejected. */
|
|
2696
2651
|
const machineGate = (trigger) => {
|
|
2697
2652
|
const bot = trigger.actor.id ? allowedSender(settings, trigger.actor) : void 0;
|
|
2698
2653
|
if (trigger.actor.id && !bot) return {
|
|
@@ -2924,7 +2879,8 @@ const byLogin = (a, b) => {
|
|
|
2924
2879
|
return x < y ? -1 : x > y ? 1 : a < b ? -1 : a > b ? 1 : 0;
|
|
2925
2880
|
};
|
|
2926
2881
|
/**
|
|
2927
|
-
*
|
|
2882
|
+
* Maps the config policy onto Settings. The allowed list is sorted here so that its order does not change
|
|
2883
|
+
* the policy version. The case of each login is kept; comparison is case-insensitive.
|
|
2928
2884
|
*/
|
|
2929
2885
|
function settingsOf(config) {
|
|
2930
2886
|
const { allowed, acceptMode, rules, admission } = config.policy;
|
|
@@ -2937,9 +2893,13 @@ function settingsOf(config) {
|
|
|
2937
2893
|
}
|
|
2938
2894
|
// src/app/credentials.ts
|
|
2939
2895
|
/**
|
|
2940
|
-
*
|
|
2896
|
+
* Resolves the selected auth mode into the environment the runtime may receive.
|
|
2897
|
+
* Names are reported, values never.
|
|
2898
|
+
*/
|
|
2899
|
+
/**
|
|
2900
|
+
* Per mode, the requirements. A requirement is met when every variable of one of its alternatives is set (non-blank).
|
|
2901
|
+
* All names are in AUTH_ENV[mode].
|
|
2941
2902
|
*/
|
|
2942
|
-
/** Per mode, the requirements; a requirement is met when every variable of one of its alternatives is set (non-blank). All names are in AUTH_ENV[mode]. */
|
|
2943
2903
|
const REQUIRED_ENV = {
|
|
2944
2904
|
"api-key": [[["ANTHROPIC_API_KEY"]]],
|
|
2945
2905
|
subscription: [[["CLAUDE_CODE_OAUTH_TOKEN"]]],
|
|
@@ -3025,13 +2985,12 @@ function assertStartupPolicy(config, credential, agentLogin, probe, now = Date.n
|
|
|
3025
2985
|
// src/app/effects.ts
|
|
3026
2986
|
/**
|
|
3027
2987
|
* Signals that an effect cannot run right now, such as when processing is paused or all worker slots are full.
|
|
3028
|
-
* Effects reschedules the row after the initial retry delay without incrementing its failure count
|
|
2988
|
+
* Effects reschedules the row after the initial retry delay without incrementing its failure count.
|
|
3029
2989
|
*/
|
|
3030
2990
|
var Later = class extends Error {};
|
|
3031
2991
|
/**
|
|
3032
|
-
* Durable SQLite-backed queue that persists side effects inside command transactions
|
|
3033
|
-
*
|
|
3034
|
-
* Changes from the reference: the constructor parameters are declared fields, assigned in the constructor body; a `cleanup` effect kind is added; comments name the Router session and WorkerExecutor.
|
|
2992
|
+
* Durable SQLite-backed queue that persists side effects inside command transactions
|
|
2993
|
+
* and dispatches them after commit.
|
|
3035
2994
|
*/
|
|
3036
2995
|
var Effects = class {
|
|
3037
2996
|
handlers = /* @__PURE__ */ new Map();
|
|
@@ -3041,7 +3000,7 @@ var Effects = class {
|
|
|
3041
3000
|
broken = /* @__PURE__ */ new Set();
|
|
3042
3001
|
seq;
|
|
3043
3002
|
scheduled = false;
|
|
3044
|
-
/** Effects run only between `start` and `drain
|
|
3003
|
+
/** Effects run only between `start` and `drain`. Rows queued before `start` wait, e.g. while setup is incomplete. */
|
|
3045
3004
|
started = false;
|
|
3046
3005
|
stopped = false;
|
|
3047
3006
|
store;
|
|
@@ -3053,7 +3012,7 @@ var Effects = class {
|
|
|
3053
3012
|
this.now = now;
|
|
3054
3013
|
this.seq = Number(store.db.prepare("SELECT IFNULL(MAX(seq), 0) AS seq FROM effect").get()?.seq ?? 0);
|
|
3055
3014
|
}
|
|
3056
|
-
/** Upserts an effect in the caller's transaction, coalescing with
|
|
3015
|
+
/** Upserts an effect in the caller's transaction, coalescing with a waiting row of the same kind and key. */
|
|
3057
3016
|
enqueue(kind, key, payload) {
|
|
3058
3017
|
this.store.db.prepare(`INSERT INTO effect (kind, key, value, seq, due, since) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(kind, key) DO UPDATE
|
|
3059
3018
|
SET value = excluded.value, generation = generation + 1, attempts = 0, error = '', due = excluded.due`).run(kind, key, JSON.stringify(payload), ++this.seq, this.now(), new Date(this.now()).toISOString());
|
|
@@ -3095,7 +3054,10 @@ var Effects = class {
|
|
|
3095
3054
|
this.dispatch();
|
|
3096
3055
|
});
|
|
3097
3056
|
}
|
|
3098
|
-
/**
|
|
3057
|
+
/**
|
|
3058
|
+
* Resolves once no effect of `kind` (or of any kind) is running, and none can start
|
|
3059
|
+
* because none is due or `limit()` has no room.
|
|
3060
|
+
*/
|
|
3099
3061
|
async idle(kind) {
|
|
3100
3062
|
for (;;) {
|
|
3101
3063
|
this.dispatch();
|
|
@@ -3187,7 +3149,10 @@ var Effects = class {
|
|
|
3187
3149
|
}
|
|
3188
3150
|
};
|
|
3189
3151
|
// src/executor/contract.ts
|
|
3190
|
-
/**
|
|
3152
|
+
/**
|
|
3153
|
+
* Idempotent: `fresh` returns, in order, the ids it has not returned before.
|
|
3154
|
+
* Duplicates within one call count once.
|
|
3155
|
+
*/
|
|
3191
3156
|
var ReadLedger = class {
|
|
3192
3157
|
seen = /* @__PURE__ */ new Set();
|
|
3193
3158
|
fresh(ids) {
|
|
@@ -3201,7 +3166,10 @@ var ReadLedger = class {
|
|
|
3201
3166
|
};
|
|
3202
3167
|
const DRAIN_MS = 2e3;
|
|
3203
3168
|
const workerKey = (ticketId) => `worker-${ticketId}`;
|
|
3204
|
-
/**
|
|
3169
|
+
/**
|
|
3170
|
+
* Mode of a ticket Input (see docs/runtime-contract.md): a person's message steers; notes, reopen markers and
|
|
3171
|
+
* a message no person wrote that waited on a ready ticket (`quiet`) are injected; everything else is a followup.
|
|
3172
|
+
*/
|
|
3205
3173
|
function inputMode(input) {
|
|
3206
3174
|
switch (input.kind) {
|
|
3207
3175
|
case "trigger": return input.human ? "steer" : input.quiet ? "inject" : "followup";
|
|
@@ -3212,7 +3180,10 @@ function inputMode(input) {
|
|
|
3212
3180
|
case "resumed": return "followup";
|
|
3213
3181
|
}
|
|
3214
3182
|
}
|
|
3215
|
-
/**
|
|
3183
|
+
/**
|
|
3184
|
+
* What is left of the limits for an Assignment that used `used` already: the SDK caps of a launch or resume,
|
|
3185
|
+
* at least 1 turn and $0.01. The host still checks the totals.
|
|
3186
|
+
*/
|
|
3216
3187
|
function remainingLimits(limits, used) {
|
|
3217
3188
|
if (!used) return { ...limits };
|
|
3218
3189
|
return {
|
|
@@ -3220,7 +3191,7 @@ function remainingLimits(limits, used) {
|
|
|
3220
3191
|
maxBudgetUsd: Math.max(.01, Math.round((limits.maxBudgetUsd - used.costUsd) * 1e6) / 1e6)
|
|
3221
3192
|
};
|
|
3222
3193
|
}
|
|
3223
|
-
/** Worker-facing text of an Input; undefined for a
|
|
3194
|
+
/** Worker-facing text of an Input; undefined for a reopen marker, which is marked delivered without being sent. */
|
|
3224
3195
|
function renderInput(input, trigger) {
|
|
3225
3196
|
switch (input.kind) {
|
|
3226
3197
|
case "trigger": return trigger ? JSON.stringify({
|
|
@@ -3304,11 +3275,10 @@ var WorkerExecutor = class {
|
|
|
3304
3275
|
this.halt(ticket.id, assignment.id);
|
|
3305
3276
|
});
|
|
3306
3277
|
}
|
|
3307
|
-
/** Ticket IDs whose worker session is open now. */
|
|
3308
3278
|
liveTickets() {
|
|
3309
3279
|
return [...this.live.keys()];
|
|
3310
3280
|
}
|
|
3311
|
-
/** Injects a host notice
|
|
3281
|
+
/** Injects a host notice, read with the next input; never starts a turn. False when no session is open. */
|
|
3312
3282
|
notify(ticketId, notice) {
|
|
3313
3283
|
const live = this.live.get(ticketId);
|
|
3314
3284
|
if (!live) return false;
|
|
@@ -3922,7 +3892,7 @@ var EventMapper = class {
|
|
|
3922
3892
|
}
|
|
3923
3893
|
};
|
|
3924
3894
|
// src/runtimes/claude/children.ts
|
|
3925
|
-
/** Tracks the runtime's child processes in <stateDir>/children.json and stops the ones an earlier host left running
|
|
3895
|
+
/** Tracks the runtime's child processes in <stateDir>/children.json and stops the ones an earlier host left running. */
|
|
3926
3896
|
var ChildRegistry = class {
|
|
3927
3897
|
file;
|
|
3928
3898
|
constructor(file) {
|
|
@@ -4036,10 +4006,12 @@ async function reapChildren(registry, options = {}) {
|
|
|
4036
4006
|
}
|
|
4037
4007
|
// src/runtimes/claude/runtime.ts
|
|
4038
4008
|
/**
|
|
4039
|
-
* Claude runtime adapter: one SDK query per session fed by a message stream.
|
|
4040
|
-
*
|
|
4041
|
-
*
|
|
4042
|
-
*
|
|
4009
|
+
* Claude runtime adapter: one SDK query per session fed by a message stream.
|
|
4010
|
+
* steer is sent into the running turn with priority next; followup is held until the running turn's result.
|
|
4011
|
+
* inject is merged into the next input sent and is never sent alone (the SDK shouldQuery flag is unused).
|
|
4012
|
+
* Message uuids are adapter UUIDs mapped to input ids. The permission mode is fixed by options.ts.
|
|
4013
|
+
* A result suppressed by interrupt does not end the newer turn:
|
|
4014
|
+
* busy and held followups wait for that turn's own result.
|
|
4043
4015
|
*/
|
|
4044
4016
|
const CLAUDE_SDK_VERSION = "0.3.321";
|
|
4045
4017
|
const INJECT_SEPARATOR = "\n\n---\n\n";
|
|
@@ -4056,7 +4028,7 @@ function bwrapProbeArgs(weakerNested) {
|
|
|
4056
4028
|
"true"
|
|
4057
4029
|
];
|
|
4058
4030
|
}
|
|
4059
|
-
/** Linux: bwrap and socat on PATH and
|
|
4031
|
+
/** Linux: bwrap and socat on PATH, and a bwrap smoke run with the /proc mount the SDK will use. */
|
|
4060
4032
|
function sandboxAvailable(platform = process.platform, weakerNested = false, run = (cmd, args) => spawnSync(cmd, args, {
|
|
4061
4033
|
stdio: "ignore",
|
|
4062
4034
|
timeout: 1e4
|
|
@@ -4498,7 +4470,7 @@ var FakeRuntime = class {
|
|
|
4498
4470
|
};
|
|
4499
4471
|
// src/app/agent.ts
|
|
4500
4472
|
/**
|
|
4501
|
-
* The
|
|
4473
|
+
* The adapter env carries only the inherited whitelist and the selected auth mode's variables.
|
|
4502
4474
|
*/
|
|
4503
4475
|
function agentEnv(credential, env) {
|
|
4504
4476
|
const values = {};
|
|
@@ -4552,7 +4524,11 @@ function createAgent(o) {
|
|
|
4552
4524
|
};
|
|
4553
4525
|
}
|
|
4554
4526
|
// src/infra/commands.ts
|
|
4555
|
-
/**
|
|
4527
|
+
/**
|
|
4528
|
+
* Subprocess runner for the hosting API program and git, with timeouts, abort signals and sanitized errors.
|
|
4529
|
+
* API paths are confined to a root. Subprocesses get a whitelisted environment.
|
|
4530
|
+
* API-only variables reach only the API program.
|
|
4531
|
+
*/
|
|
4556
4532
|
const TIMEOUT_MS = 9e4;
|
|
4557
4533
|
const MAX_BUFFER = 16777216;
|
|
4558
4534
|
var CliError = class extends Error {
|
|
@@ -4588,7 +4564,6 @@ function ended(error) {
|
|
|
4588
4564
|
if (typeof code === "number") return `exited with code ${code}`;
|
|
4589
4565
|
return typeof code === "string" ? `could not run: ${code}` : "failed";
|
|
4590
4566
|
}
|
|
4591
|
-
/** Parses array elements against a schema, skipping invalid items and incrementing `tally.skipped`. */
|
|
4592
4567
|
function validItems(item, list, tally) {
|
|
4593
4568
|
const items = list.flatMap((value) => {
|
|
4594
4569
|
const parsed = item.safeParse(value);
|
|
@@ -4650,7 +4625,6 @@ var Commands = class Commands {
|
|
|
4650
4625
|
runApi(args) {
|
|
4651
4626
|
return this.run(this.options.apiProgram, args, void 0, this.options.apiEnv ?? {});
|
|
4652
4627
|
}
|
|
4653
|
-
/** Runs the API program and parses its JSON output. */
|
|
4654
4628
|
async call(args) {
|
|
4655
4629
|
return JSON.parse(await this.runApi(args));
|
|
4656
4630
|
}
|
|
@@ -4690,33 +4664,35 @@ var Commands = class Commands {
|
|
|
4690
4664
|
// src/app/kv.ts
|
|
4691
4665
|
/** Define a single typed KV key; external Source plugins use `record` and `family` to type their own keys. */
|
|
4692
4666
|
const record = (key) => key;
|
|
4693
|
-
/** Define a KV key family whose keys join `prefix` and name parts with `:` (or a custom `format`). */
|
|
4694
4667
|
function family(prefix, format = (...name) => name.join(":")) {
|
|
4695
4668
|
return Object.assign((...name) => `${prefix}${format(...name)}`, { prefix });
|
|
4696
4669
|
}
|
|
4697
4670
|
const KV = {
|
|
4698
|
-
/**
|
|
4671
|
+
/**
|
|
4672
|
+
* Runtime health: the HealthBoard snapshot the daemon writes each pass (at most every 5 s),
|
|
4673
|
+
* read by the status CLI.
|
|
4674
|
+
*/
|
|
4699
4675
|
health: { snapshot: record("health") },
|
|
4700
|
-
/** Store: current database schema version. */
|
|
4701
4676
|
store: { schema: record("schema") },
|
|
4702
|
-
/**
|
|
4677
|
+
/**
|
|
4678
|
+
* Runtime: the Router session's ID and the prompt version it was opened under, and the readiness marker
|
|
4679
|
+
* the daemon writes once started and deletes first on stop (`health --ready`).
|
|
4680
|
+
*/
|
|
4703
4681
|
runtime: {
|
|
4704
4682
|
masterId: record("master-id"),
|
|
4705
4683
|
routerPrompt: record("router-prompt-version"),
|
|
4706
4684
|
ready: record("runtime-ready")
|
|
4707
4685
|
},
|
|
4708
|
-
/**
|
|
4686
|
+
/**
|
|
4687
|
+
* Sessions: prompt revisions each Session has worked under, oldest first.
|
|
4688
|
+
* The last entry (`.at(-1)`) is its active revision.
|
|
4689
|
+
*/
|
|
4709
4690
|
sessions: { prompts: family("session-prompts:") },
|
|
4710
4691
|
/** Per-Source resolved display names mapping a named ID (such as a chat ID) to its display name. */
|
|
4711
4692
|
sources: { labels: family("source-labels:") }
|
|
4712
4693
|
};
|
|
4713
4694
|
// src/infra/text.ts
|
|
4714
|
-
/**
|
|
4715
|
-
* Word counting and string formatting utilities for message limits and templates.
|
|
4716
|
-
* Changes from the reference: `IM_WORDS`, `checkedImText` and `sameName` are removed; `policy/actors.ts` is the single `sameName`.
|
|
4717
|
-
*/
|
|
4718
4695
|
const words = new Intl.Segmenter("zh", { granularity: "word" });
|
|
4719
|
-
/** Counts word-like segments across Chinese, English, and mixed text, ignoring punctuation. */
|
|
4720
4696
|
function wordCount(text) {
|
|
4721
4697
|
let count = 0;
|
|
4722
4698
|
for (const segment of words.segment(text)) if (segment.isWordLike) count++;
|
|
@@ -4724,14 +4700,22 @@ function wordCount(text) {
|
|
|
4724
4700
|
}
|
|
4725
4701
|
const fill = (template, values) => template.replace(/\{(\w+)\}/g, (_, name) => values[name] ?? "");
|
|
4726
4702
|
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4727
|
-
/**
|
|
4703
|
+
/**
|
|
4704
|
+
* Lines that credit the AI product as an author: a Co-authored-by trailer whose name has the word Claude or whose
|
|
4705
|
+
* address is @anthropic.com, and a "Generated with … Claude" line. Case-insensitive, one line at a time.
|
|
4706
|
+
* scripts/check-commits.ts uses the same predicate.
|
|
4707
|
+
*/
|
|
4728
4708
|
const AI_ATTRIBUTION_PATTERNS = [
|
|
4729
4709
|
/^\s*co-authored-by:[^<\r\n]*\bclaude\b/i,
|
|
4730
4710
|
/^\s*co-authored-by:.*@anthropic\.com\b/i,
|
|
4731
4711
|
/\bgenerated with\b.*\bclaude\b/i
|
|
4732
4712
|
];
|
|
4733
4713
|
const isAiAttributionLine = (line) => AI_ATTRIBUTION_PATTERNS.some((pattern) => pattern.test(line));
|
|
4734
|
-
/**
|
|
4714
|
+
/**
|
|
4715
|
+
* Removes AI attribution lines; returns `text` itself when there are none.
|
|
4716
|
+
* Blank lines the removal leaves doubled or leading are dropped; trailing blank lines are trimmed.
|
|
4717
|
+
* One final newline is kept when `text` had one.
|
|
4718
|
+
*/
|
|
4735
4719
|
function stripAiAttribution(text) {
|
|
4736
4720
|
const lines = text.split("\n");
|
|
4737
4721
|
if (!lines.some(isAiAttributionLine)) return text;
|
|
@@ -4752,7 +4736,7 @@ function stripAiAttribution(text) {
|
|
|
4752
4736
|
return body !== "" && text.endsWith("\n") ? body + "\n" : body;
|
|
4753
4737
|
}
|
|
4754
4738
|
// src/sources/host.ts
|
|
4755
|
-
/** Fixed English
|
|
4739
|
+
/** Fixed English templates; the panel localizes each `code` in the interface language. */
|
|
4756
4740
|
const HEALTH_TEXT = {
|
|
4757
4741
|
sourceOff: "Off",
|
|
4758
4742
|
pollFailures: "{count} consecutive failed polls; last: {error}"
|
|
@@ -4765,10 +4749,7 @@ function health(state, code, values = {}) {
|
|
|
4765
4749
|
...Object.keys(values).length ? { values } : {}
|
|
4766
4750
|
};
|
|
4767
4751
|
}
|
|
4768
|
-
/**
|
|
4769
|
-
* Builds the context for one Source kind. Health reports go to the entries of the registration current when the context
|
|
4770
|
-
* is built, becoming no-ops once that registration ends.
|
|
4771
|
-
*/
|
|
4752
|
+
/** Health reports go to the registration current when the context is built; they become no-ops once it ends. */
|
|
4772
4753
|
function sourceContext(deps, kind) {
|
|
4773
4754
|
const { store } = deps;
|
|
4774
4755
|
const report = deps.sources.reporter(kind);
|
|
@@ -4827,19 +4808,24 @@ function sourceContext(deps, kind) {
|
|
|
4827
4808
|
};
|
|
4828
4809
|
}
|
|
4829
4810
|
// src/sources/poll.ts
|
|
4830
|
-
/** Shared polling helpers for subscribed threads
|
|
4811
|
+
/** Shared polling helpers for subscribed threads: overlap-safe cursors and delays. */
|
|
4831
4812
|
/** How long a subscribed Thread with no live Ticket continues to be polled after its last Trigger or Ticket end. */
|
|
4832
4813
|
const POLL_HORIZON_MS = 12096e5;
|
|
4833
|
-
/** True when a subscribed thread has a live ticket or recent activity/completion within `POLL_HORIZON_MS`. */
|
|
4834
4814
|
function polled(ctx, thread, now = Date.now()) {
|
|
4835
4815
|
const tickets = thread.subscriptions.map((id) => ctx.tickets.get(id));
|
|
4836
4816
|
if (tickets.some((ticket) => !!ticket && liveTicket(ticket))) return true;
|
|
4837
4817
|
const times = [thread.lastAt ?? thread.createdAt, ...tickets.map((ticket) => ticket?.ended?.at ?? "")].map(Date.parse).filter(Number.isFinite);
|
|
4838
4818
|
return now - Math.max(...times) < POLL_HORIZON_MS;
|
|
4839
4819
|
}
|
|
4840
|
-
/**
|
|
4820
|
+
/**
|
|
4821
|
+
* Steps a poll start timestamp back by 5 seconds so late-arriving remote events are not missed; deterministic trigger
|
|
4822
|
+
* IDs deduplicate overlaps.
|
|
4823
|
+
*/
|
|
4841
4824
|
const cursorAt = (started) => (/* @__PURE__ */ new Date(Date.parse(started) - 5e3)).toISOString();
|
|
4842
|
-
/**
|
|
4825
|
+
/**
|
|
4826
|
+
* Polls each subscribed Thread of `source` within the horizon independently; records failures on the Thread as
|
|
4827
|
+
* `pollError` (unless `stopped()`) and clears `pollError` on success.
|
|
4828
|
+
*/
|
|
4843
4829
|
async function pollThreads$1(ctx, source, stopped, poll, skip = () => false, now = () => Date.now()) {
|
|
4844
4830
|
for (const thread of ctx.threads.subscribed(source)) {
|
|
4845
4831
|
if (stopped()) return;
|
|
@@ -4859,7 +4845,7 @@ const systemTimers$1 = {
|
|
|
4859
4845
|
clearTimeout(handle);
|
|
4860
4846
|
}
|
|
4861
4847
|
};
|
|
4862
|
-
/** Waits `ms` or until `signal` aborts
|
|
4848
|
+
/** Waits `ms` or until `signal` aborts; the injected timers let tests drive polling loops. */
|
|
4863
4849
|
function pause(ms, signal, timers = systemTimers$1) {
|
|
4864
4850
|
return new Promise((resolve) => {
|
|
4865
4851
|
if (signal.aborted) {
|
|
@@ -4878,7 +4864,10 @@ function pause(ms, signal, timers = systemTimers$1) {
|
|
|
4878
4864
|
});
|
|
4879
4865
|
}
|
|
4880
4866
|
// src/sources/github/trigger.ts
|
|
4881
|
-
/**
|
|
4867
|
+
/**
|
|
4868
|
+
* Normalizes GitHub issues, pull requests, comments, reviews, review comments and CI transitions into domain Triggers.
|
|
4869
|
+
* Thread, trigger and channel keys include the repository (Thread id <owner>/<name>#<number>).
|
|
4870
|
+
*/
|
|
4882
4871
|
const SOURCE = "github";
|
|
4883
4872
|
/** The kind has no config keys; `{}` parses, so the kind is enabled under the Runtime's empty configs reader. */
|
|
4884
4873
|
const GithubConfigSchema = z.record(z.string(), z.never());
|
|
@@ -4902,7 +4891,7 @@ const githubChannel = (repository, number, url) => ({
|
|
|
4902
4891
|
...url ? { url } : {}
|
|
4903
4892
|
}
|
|
4904
4893
|
});
|
|
4905
|
-
/** Parses
|
|
4894
|
+
/** Parses an Issue, PR, or review-comment URL in a configured repository; undefined for anything else. */
|
|
4906
4895
|
function githubAddress(url, repositories) {
|
|
4907
4896
|
const match = /^https:\/\/(?:api\.github\.com\/repos|github\.com)\/([^/]+\/[^/]+)\/(issues|pulls?)\/(\d+)\/?(?:#discussion_r(\d+)|#[\w-]*)?$/i.exec(url);
|
|
4908
4897
|
if (!match) return void 0;
|
|
@@ -4923,10 +4912,7 @@ const UserSchema = z.looseObject({
|
|
|
4923
4912
|
login: "ghost",
|
|
4924
4913
|
type: "User"
|
|
4925
4914
|
});
|
|
4926
|
-
/**
|
|
4927
|
-
* A GitHub comment or review; a review carries `state` and `commit_id`, and an inline review comment carries its file,
|
|
4928
|
-
* line, diff hunk, and `in_reply_to_id` (the root comment ID of its review thread).
|
|
4929
|
-
*/
|
|
4915
|
+
/** A GitHub comment or review; `in_reply_to_id` is the root comment ID of its review thread. */
|
|
4930
4916
|
const CommentSchema$1 = z.looseObject({
|
|
4931
4917
|
id: z.number(),
|
|
4932
4918
|
body: z.string().nullish(),
|
|
@@ -4943,7 +4929,7 @@ const CommentSchema$1 = z.looseObject({
|
|
|
4943
4929
|
diff_hunk: z.string().nullish(),
|
|
4944
4930
|
in_reply_to_id: z.number().nullish()
|
|
4945
4931
|
});
|
|
4946
|
-
/**
|
|
4932
|
+
/** GitHub sets `pull_request` when the number is a pull request. */
|
|
4947
4933
|
const IssueSchema = CommentSchema$1.extend({
|
|
4948
4934
|
number: z.number(),
|
|
4949
4935
|
title: z.string(),
|
|
@@ -4952,12 +4938,8 @@ const IssueSchema = CommentSchema$1.extend({
|
|
|
4952
4938
|
html_url: z.string(),
|
|
4953
4939
|
pull_request: z.looseObject({ url: z.string() }).nullish()
|
|
4954
4940
|
});
|
|
4955
|
-
/**
|
|
4956
|
-
* True when GitHub reports the account as a regular user rather than a bot or GitHub App.
|
|
4957
|
-
* Review bots running under ordinary user accounts count as human here; the prompts identify which accounts are automated.
|
|
4958
|
-
*/
|
|
4941
|
+
/** Review bots under ordinary user accounts count as human here; the prompts identify automated accounts. */
|
|
4959
4942
|
const isHumanLogin = (user) => !/\[bot\]$/i.test(user.login) && (!user.type || user.type === "User");
|
|
4960
|
-
/** Formats an inline review comment's file:line and the trailing lines of its diff hunk, which GitHub ends at the commented line. */
|
|
4961
4943
|
function reviewPlace(comment) {
|
|
4962
4944
|
const line = comment.line ?? comment.original_line;
|
|
4963
4945
|
const hunk = (comment.diff_hunk ?? "").split("\n").slice(-3).join("\n");
|
|
@@ -4978,7 +4960,7 @@ const author = (user, agent) => ({
|
|
|
4978
4960
|
},
|
|
4979
4961
|
human: isHumanLogin(user) && !sameName(user.login, agent)
|
|
4980
4962
|
});
|
|
4981
|
-
/**
|
|
4963
|
+
/** Text is empty for a bodyless comment or a `COMMENTED` review without a body. */
|
|
4982
4964
|
function commentTrigger(comment, ref, kind, agent) {
|
|
4983
4965
|
const at = comment.updated_at ?? comment.submitted_at ?? comment.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4984
4966
|
const version = comment.updated_at ?? comment.submitted_at ?? "";
|
|
@@ -5010,10 +4992,7 @@ function commentTrigger(comment, ref, kind, agent) {
|
|
|
5010
4992
|
}
|
|
5011
4993
|
};
|
|
5012
4994
|
}
|
|
5013
|
-
/**
|
|
5014
|
-
* Builds the trigger for a newly opened Issue's title and body.
|
|
5015
|
-
* The ID is the same as when Issue bodies were stored previously, so already-stored bodies are not ingested again.
|
|
5016
|
-
*/
|
|
4995
|
+
/** The ID matches the one used when Issue bodies were stored earlier, so stored bodies are not ingested again. */
|
|
5017
4996
|
function issueTrigger(issue, repository, agent) {
|
|
5018
4997
|
const body = issue.body ?? "";
|
|
5019
4998
|
return {
|
|
@@ -5037,10 +5016,7 @@ function issueTrigger(issue, repository, agent) {
|
|
|
5037
5016
|
}
|
|
5038
5017
|
};
|
|
5039
5018
|
}
|
|
5040
|
-
/**
|
|
5041
|
-
* Builds the trigger for a newly opened PR's title and description.
|
|
5042
|
-
* PR and Issue numbers share one sequence in GitHub and never collide, so this reuses `issueTrigger`'s `github-issue` ID format.
|
|
5043
|
-
*/
|
|
5019
|
+
/** PR and Issue numbers never collide, so the PR trigger reuses `issueTrigger`'s `github-issue` ID. */
|
|
5044
5020
|
function pullTrigger(pull, repository, agent) {
|
|
5045
5021
|
return {
|
|
5046
5022
|
...issueTrigger(pull, repository, agent),
|
|
@@ -5081,7 +5057,10 @@ function ciTrigger(repository, pr, revision, summary, at) {
|
|
|
5081
5057
|
};
|
|
5082
5058
|
}
|
|
5083
5059
|
// src/sources/github/read.ts
|
|
5084
|
-
/**
|
|
5060
|
+
/**
|
|
5061
|
+
* Read-only GitHub repository, conversation, and CI check inspection for assessments and workers.
|
|
5062
|
+
* Readers take commands scoped to one repository; the thread context reader picks it from meta.repository.
|
|
5063
|
+
*/
|
|
5085
5064
|
const CheckSchema = z.looseObject({
|
|
5086
5065
|
name: z.string(),
|
|
5087
5066
|
status: z.string(),
|
|
@@ -5171,7 +5150,10 @@ async function pullChecks(commands, head, tally) {
|
|
|
5171
5150
|
})).sort((a, b) => a.context.localeCompare(b.context))
|
|
5172
5151
|
};
|
|
5173
5152
|
}
|
|
5174
|
-
/**
|
|
5153
|
+
/**
|
|
5154
|
+
* Formats a GitHub comment, review, or issue as a model-facing ContextMessage, marking bots and GitHub Apps because
|
|
5155
|
+
* bot messages never authorize work.
|
|
5156
|
+
*/
|
|
5175
5157
|
function githubMessage(item, text, id) {
|
|
5176
5158
|
const line = item.line ?? item.original_line;
|
|
5177
5159
|
return {
|
|
@@ -5262,7 +5244,7 @@ async function readGithub(commands, resource) {
|
|
|
5262
5244
|
...listed.length === 20 ? { next_resource: `github/${kind}/${number}/${section}/${Number(page) + 1}` } : {}
|
|
5263
5245
|
};
|
|
5264
5246
|
}
|
|
5265
|
-
/**
|
|
5247
|
+
/** One page of an Issue or PR conversation; page 1 starts with the Issue/PR description. */
|
|
5266
5248
|
async function conversationPage(commands, number, page) {
|
|
5267
5249
|
const messages = [];
|
|
5268
5250
|
if (page === 1) {
|
|
@@ -5279,10 +5261,7 @@ async function conversationPage(commands, number, page) {
|
|
|
5279
5261
|
...tally.skipped ? { skipped: tally.skipped } : {}
|
|
5280
5262
|
};
|
|
5281
5263
|
}
|
|
5282
|
-
/**
|
|
5283
|
-
* Context reader for a GitHub Trigger: reads the review thread for an inline review comment (whose first comment is its
|
|
5284
|
-
* root and parent), or the Issue/PR conversation by page otherwise (whose first page is its root and parent).
|
|
5285
|
-
*/
|
|
5264
|
+
/** Reads the review thread of an inline review comment; otherwise the Issue/PR conversation by page. */
|
|
5286
5265
|
function githubThreadContext(commands, repositories) {
|
|
5287
5266
|
return {
|
|
5288
5267
|
messageId: (trigger) => trigger.url ?? "",
|
|
@@ -5313,18 +5292,21 @@ function githubThreadContext(commands, repositories) {
|
|
|
5313
5292
|
};
|
|
5314
5293
|
}
|
|
5315
5294
|
// src/sources/github/channel.ts
|
|
5316
|
-
/**
|
|
5295
|
+
/**
|
|
5296
|
+
* GitHub outbound channel: posts issue/PR comments and review thread replies under the agent account.
|
|
5297
|
+
* The reply goes to the repository named in the address, which must be configured.
|
|
5298
|
+
*/
|
|
5317
5299
|
const CommentSchema = z.looseObject({
|
|
5318
5300
|
id: z.number(),
|
|
5319
5301
|
body: z.string().nullish(),
|
|
5320
5302
|
in_reply_to_id: z.number().nullish(),
|
|
5321
5303
|
user: z.looseObject({ login: z.string() }).nullish()
|
|
5322
5304
|
});
|
|
5323
|
-
/**
|
|
5305
|
+
/** A 410, or a locked 403, means gone; 404 is retried because it can mean lost permission on a private repo. */
|
|
5324
5306
|
const gone = (error) => error instanceof CliError && (error.code === 410 || error.code === 403 && /locked/i.test(error.message));
|
|
5325
5307
|
const missing = (error) => error instanceof CliError && (error.code === 404 || error.code === 410);
|
|
5326
5308
|
const MARKER = (id) => `<!-- superagent-claude:${id} -->`;
|
|
5327
|
-
/**
|
|
5309
|
+
/** Embeds an HTML marker so a retried post does not create a duplicate comment. */
|
|
5328
5310
|
function githubChannelAdapter(options) {
|
|
5329
5311
|
return {
|
|
5330
5312
|
allowed: (address) => !!configured(options.repositories, address.repository ?? "") && Number(address.number) > 0,
|
|
@@ -5362,7 +5344,7 @@ function githubChannelAdapter(options) {
|
|
|
5362
5344
|
};
|
|
5363
5345
|
}
|
|
5364
5346
|
// src/sources/github/pull.ts
|
|
5365
|
-
/** Pull request reads
|
|
5347
|
+
/** Pull request reads and lifecycle observation, including the auto-merge fact. */
|
|
5366
5348
|
const PullSchema = z.looseObject({
|
|
5367
5349
|
title: z.string().optional(),
|
|
5368
5350
|
state: z.string(),
|
|
@@ -5409,7 +5391,11 @@ function observePull(ctx, pr, now) {
|
|
|
5409
5391
|
return ended;
|
|
5410
5392
|
}
|
|
5411
5393
|
// src/sources/github/source.ts
|
|
5412
|
-
/**
|
|
5394
|
+
/**
|
|
5395
|
+
* GitHub polling Source: the agent account's notifications, the PRs of open tickets with their CI,
|
|
5396
|
+
* and subscribed Threads, polled per repository. Each repository has its own cursors and health entry
|
|
5397
|
+
* (github:<owner>/<name>). Access and push permission are checked at start; an inaccessible repository is not polled.
|
|
5398
|
+
*/
|
|
5413
5399
|
const POLL_INTERVAL_MS = 6e4;
|
|
5414
5400
|
const GITHUB_STATE = {
|
|
5415
5401
|
access: family("repo-access:"),
|
|
@@ -5419,7 +5405,10 @@ const GITHUB_STATE = {
|
|
|
5419
5405
|
cursor: family("github-cursor:", (repository, number) => `${repository}#${number}`),
|
|
5420
5406
|
ci: family("github-ci:", (repository, number) => `${repository}#${number}`)
|
|
5421
5407
|
};
|
|
5422
|
-
/**
|
|
5408
|
+
/**
|
|
5409
|
+
* Reports a failed poll with its consecutive count: `ready` until `FAILURES_BEFORE_ERROR` consecutive failures,
|
|
5410
|
+
* `error` from then on.
|
|
5411
|
+
*/
|
|
5423
5412
|
function pollFailure(failures, error) {
|
|
5424
5413
|
return health(failures >= 3 ? "error" : "ready", "pollFailures", {
|
|
5425
5414
|
count: String(failures),
|
|
@@ -5432,10 +5421,10 @@ const NotificationSchema = z.looseObject({
|
|
|
5432
5421
|
subject: z.looseObject({ url: z.string().nullish() })
|
|
5433
5422
|
});
|
|
5434
5423
|
const RETRY_WINDOW_MS = 864e5;
|
|
5435
|
-
/**
|
|
5424
|
+
/** Only 410 counts as deleted; 404 is retried because it can mean revoked access to a private repository. */
|
|
5436
5425
|
const deleted = (error) => error instanceof CliError && error.code === 410;
|
|
5437
5426
|
const stopped = (round) => round.signal.aborted;
|
|
5438
|
-
/**
|
|
5427
|
+
/** Deleted or abandoned threads are recorded without failing the round. */
|
|
5439
5428
|
function failed(ctx, round, number, error, abandoned = false) {
|
|
5440
5429
|
if (deleted(error)) round.dropped.push(number);
|
|
5441
5430
|
else if (abandoned) round.abandoned.push(number);
|
|
@@ -5478,7 +5467,6 @@ async function pollRepository(ctx, poll) {
|
|
|
5478
5467
|
}, poll.repository);
|
|
5479
5468
|
return interval;
|
|
5480
5469
|
}
|
|
5481
|
-
/** Reads notifications since the cursor and each Issue or PR they name, advancing the cursor atomically with the retry map for failed reads; deleted (410) Issues are dropped once their error is recorded. */
|
|
5482
5470
|
async function pollNotifications(ctx, round) {
|
|
5483
5471
|
const { state } = ctx;
|
|
5484
5472
|
const { repository, login } = round;
|
|
@@ -5676,7 +5664,7 @@ function nameThread(ctx, repository, number, title) {
|
|
|
5676
5664
|
facts: { number: { value: `#${number}` } }
|
|
5677
5665
|
});
|
|
5678
5666
|
}
|
|
5679
|
-
/**
|
|
5667
|
+
/** A deleted (410) Issue is still polled within the horizon and does not fail the round. */
|
|
5680
5668
|
async function pollThreads(ctx, round) {
|
|
5681
5669
|
const skip = (thread) => {
|
|
5682
5670
|
const parsed = parseThreadId(thread.ref.id);
|
|
@@ -5913,7 +5901,11 @@ function githubSource(options) {
|
|
|
5913
5901
|
};
|
|
5914
5902
|
}
|
|
5915
5903
|
// src/sources/github/delivery.ts
|
|
5916
|
-
/**
|
|
5904
|
+
/**
|
|
5905
|
+
* Host-side pull request writes under the agent account: open or adopt the PR of a branch, reply in a review thread
|
|
5906
|
+
* and resolve it, enable auto-merge (never merge), close a PR, and read the push permission.
|
|
5907
|
+
* Only gh api REST and graphql calls.
|
|
5908
|
+
*/
|
|
5917
5909
|
const PullLite = z.looseObject({
|
|
5918
5910
|
number: z.number(),
|
|
5919
5911
|
html_url: z.string(),
|
|
@@ -5989,9 +5981,17 @@ function githubPullRequests(commands) {
|
|
|
5989
5981
|
};
|
|
5990
5982
|
}
|
|
5991
5983
|
// src/app/sources.ts
|
|
5992
|
-
/**
|
|
5984
|
+
/**
|
|
5985
|
+
* The Source kinds this build installs: the GitHub Source when SUPERAGENT_CLAUDE_GITHUB_TOKEN is set.
|
|
5986
|
+
* The token reaches only the gh calls, as GH_TOKEN. A missing or blank token refuses startup.
|
|
5987
|
+
* The token also authenticates the host's clone and fetch calls and pull request writes, through an
|
|
5988
|
+
* env-scoped header that never reaches worker sessions or git config.
|
|
5989
|
+
*/
|
|
5993
5990
|
const GITHUB_TOKEN_ENV = "SUPERAGENT_CLAUDE_GITHUB_TOKEN";
|
|
5994
|
-
/**
|
|
5991
|
+
/**
|
|
5992
|
+
* Repository access for git workspaces: remote URLs carry no credentials.
|
|
5993
|
+
* The token authenticates only the clone and fetch calls, through an env-scoped header.
|
|
5994
|
+
*/
|
|
5995
5995
|
function repositoryAccess(env) {
|
|
5996
5996
|
const token = env[GITHUB_TOKEN_ENV]?.trim();
|
|
5997
5997
|
const encoded = token ? Buffer.from(`x-access-token:${token}`).toString("base64") : "";
|
|
@@ -6044,18 +6044,17 @@ function installedSources(config, env, options = {}) {
|
|
|
6044
6044
|
}
|
|
6045
6045
|
// src/executor/recovery.ts
|
|
6046
6046
|
/**
|
|
6047
|
-
* Restart recovery:
|
|
6048
|
-
*
|
|
6049
|
-
*
|
|
6050
|
-
*
|
|
6051
|
-
*
|
|
6052
|
-
* process's sessions and background work is dropped because they ended with that process; the note text stays in core.
|
|
6047
|
+
* Restart recovery: a running Assignment is ended with `Engine.assignmentInterrupted`, then relaunched with the
|
|
6048
|
+
* recovery note as its first Input, unless its latest report settled it (`waiting`, `ready`, `blocked`, `failed`)
|
|
6049
|
+
* and its worker caught up. A settled one keeps its session and takes no slot or turn until an Input arrives.
|
|
6050
|
+
* Launches coalesce per Assignment, so none is queued twice.
|
|
6051
|
+
* The previous process's sessions and background work are not stopped here; they ended with that process.
|
|
6053
6052
|
* `interruptRunning` records the same at a graceful stop (SIGTERM), so the next start only launches what it queued.
|
|
6054
6053
|
*/
|
|
6055
6054
|
function oldestFirst(tickets) {
|
|
6056
6055
|
return [...tickets.open()].reverse().sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
6057
6056
|
}
|
|
6058
|
-
/** Recovers open tickets oldest first
|
|
6057
|
+
/** Recovers open tickets oldest first. `open()` lists newest first, so ties on createdAt keep creation order. */
|
|
6059
6058
|
function recover(host) {
|
|
6060
6059
|
const tickets = oldestFirst(host.tickets);
|
|
6061
6060
|
for (const ticket of tickets) {
|
|
@@ -6065,7 +6064,10 @@ function recover(host) {
|
|
|
6065
6064
|
}
|
|
6066
6065
|
return tickets.map((ticket) => ticket.id);
|
|
6067
6066
|
}
|
|
6068
|
-
/**
|
|
6067
|
+
/**
|
|
6068
|
+
* At a graceful stop: every running Assignment is handled with `assignmentInterrupted` (oldest ticket first),
|
|
6069
|
+
* as recovery would at the next start. Returns the ids of the tickets that had one.
|
|
6070
|
+
*/
|
|
6069
6071
|
function interruptRunning(host) {
|
|
6070
6072
|
const interrupted = [];
|
|
6071
6073
|
for (const ticket of oldestFirst(host.tickets)) {
|
|
@@ -6077,7 +6079,6 @@ function interruptRunning(host) {
|
|
|
6077
6079
|
return interrupted;
|
|
6078
6080
|
}
|
|
6079
6081
|
// src/executor/slots.ts
|
|
6080
|
-
/** In-memory worker slot accounting. */
|
|
6081
6082
|
var Slots = class {
|
|
6082
6083
|
max;
|
|
6083
6084
|
held = /* @__PURE__ */ new Map();
|
|
@@ -6085,7 +6086,7 @@ var Slots = class {
|
|
|
6085
6086
|
constructor(max) {
|
|
6086
6087
|
this.max = max;
|
|
6087
6088
|
}
|
|
6088
|
-
/**
|
|
6089
|
+
/** A ticket that already holds a slot gets true without taking another. */
|
|
6089
6090
|
acquire(ticketId, kind, human) {
|
|
6090
6091
|
if (this.held.has(ticketId)) return true;
|
|
6091
6092
|
let normal = 0;
|
|
@@ -6111,16 +6112,15 @@ var Slots = class {
|
|
|
6111
6112
|
this.waiting.add(ticketId);
|
|
6112
6113
|
return false;
|
|
6113
6114
|
}
|
|
6114
|
-
/**
|
|
6115
|
+
/** Idempotent; also forgets the ticket's waiting wake. */
|
|
6115
6116
|
release(ticketId) {
|
|
6116
6117
|
this.held.delete(ticketId);
|
|
6117
6118
|
this.waiting.delete(ticketId);
|
|
6118
6119
|
}
|
|
6119
|
-
/** Whether ticketId currently holds a slot. */
|
|
6120
6120
|
holds(ticketId) {
|
|
6121
6121
|
return this.held.has(ticketId);
|
|
6122
6122
|
}
|
|
6123
|
-
/**
|
|
6123
|
+
/** Held slots, normal and extra. */
|
|
6124
6124
|
get used() {
|
|
6125
6125
|
return this.held.size;
|
|
6126
6126
|
}
|
|
@@ -6132,7 +6132,6 @@ var Slots = class {
|
|
|
6132
6132
|
}
|
|
6133
6133
|
};
|
|
6134
6134
|
// src/executor/worker-store.ts
|
|
6135
|
-
/** Reads and writes the worker table. */
|
|
6136
6135
|
var WorkerStore = class {
|
|
6137
6136
|
store;
|
|
6138
6137
|
now;
|
|
@@ -6140,7 +6139,6 @@ var WorkerStore = class {
|
|
|
6140
6139
|
this.store = store;
|
|
6141
6140
|
this.now = now;
|
|
6142
6141
|
}
|
|
6143
|
-
/** Returns the worker record of ticketId, if any. */
|
|
6144
6142
|
get(ticketId) {
|
|
6145
6143
|
const row = this.store.db.prepare("SELECT * FROM worker WHERE ticket_id = ?").get(ticketId);
|
|
6146
6144
|
if (row === void 0) return void 0;
|
|
@@ -6158,7 +6156,6 @@ var WorkerStore = class {
|
|
|
6158
6156
|
updatedAt: String(row.updated_at)
|
|
6159
6157
|
};
|
|
6160
6158
|
}
|
|
6161
|
-
/** Inserts or replaces the worker record of `record.ticketId` and stamps it with the current time. */
|
|
6162
6159
|
put(record) {
|
|
6163
6160
|
this.store.db.prepare(`INSERT INTO worker (ticket_id, assignment_id, runtime, session_id, repository, cwd, branch, push_remote, turns, cost_usd, prompt_version, updated_at)
|
|
6164
6161
|
VALUES (?,?,?,?,?,?,?,NULL,?,?,?,?)
|
|
@@ -6166,19 +6163,19 @@ var WorkerStore = class {
|
|
|
6166
6163
|
repository = excluded.repository, cwd = excluded.cwd, branch = excluded.branch, push_remote = excluded.push_remote, turns = excluded.turns,
|
|
6167
6164
|
cost_usd = excluded.cost_usd, prompt_version = excluded.prompt_version, updated_at = excluded.updated_at`).run(record.ticketId, record.assignmentId, record.runtime, record.sessionId, record.repository, record.cwd, record.branch, record.turns, record.costUsd, record.promptVersion, new Date(this.now()).toISOString());
|
|
6168
6165
|
}
|
|
6169
|
-
/** Deletes the worker record of ticketId. */
|
|
6170
6166
|
clear(ticketId) {
|
|
6171
6167
|
this.store.db.prepare("DELETE FROM worker WHERE ticket_id = ?").run(ticketId);
|
|
6172
6168
|
}
|
|
6173
|
-
/** Records the runtime session id of ticketId's worker record. */
|
|
6174
6169
|
setSession(ticketId, sessionId) {
|
|
6175
6170
|
this.store.db.prepare("UPDATE worker SET session_id = ?, updated_at = ? WHERE ticket_id = ?").run(sessionId, new Date(this.now()).toISOString(), ticketId);
|
|
6176
6171
|
}
|
|
6177
|
-
/** Records the prompt version the ticket's worker record was opened with. */
|
|
6178
6172
|
setPromptVersion(ticketId, promptVersion) {
|
|
6179
6173
|
this.store.db.prepare("UPDATE worker SET prompt_version = ?, updated_at = ? WHERE ticket_id = ?").run(promptVersion, new Date(this.now()).toISOString(), ticketId);
|
|
6180
6174
|
}
|
|
6181
|
-
/**
|
|
6175
|
+
/**
|
|
6176
|
+
* A record of another Assignment starts over from zero under assignmentId.
|
|
6177
|
+
* Returns the totals (the given values when there is no record).
|
|
6178
|
+
*/
|
|
6182
6179
|
addUsage(ticketId, assignmentId, turns, costUsd) {
|
|
6183
6180
|
this.store.db.prepare(`UPDATE worker SET turns = CASE WHEN assignment_id = ? THEN turns + ? ELSE ? END,
|
|
6184
6181
|
cost_usd = CASE WHEN assignment_id = ? THEN cost_usd + ? ELSE ? END, assignment_id = ?, updated_at = ? WHERE ticket_id = ?`).run(assignmentId, turns, turns, assignmentId, costUsd, costUsd, assignmentId, new Date(this.now()).toISOString(), ticketId);
|
|
@@ -6193,7 +6190,13 @@ var WorkerStore = class {
|
|
|
6193
6190
|
}
|
|
6194
6191
|
};
|
|
6195
6192
|
// src/executor/workspaces.ts
|
|
6196
|
-
/**
|
|
6193
|
+
/**
|
|
6194
|
+
* Worker workspaces: git worktrees on agent branches over one shared clone per repository,
|
|
6195
|
+
* and scratch directories for fake runs.
|
|
6196
|
+
* Clone and fetch get the injected access env (authentication), and errors mask its secrets.
|
|
6197
|
+
* Publishing pushes an agent branch without force, adding co-author trailers only to commits not yet pushed.
|
|
6198
|
+
* Access errors carry the HTTP status when one is known. A transient push failure is retried up to 3 times (2 s, 5 s).
|
|
6199
|
+
*/
|
|
6197
6200
|
var RepositoryAccessError = class extends Error {
|
|
6198
6201
|
repository;
|
|
6199
6202
|
reason;
|
|
@@ -6206,14 +6209,14 @@ var RepositoryAccessError = class extends Error {
|
|
|
6206
6209
|
this.status = status;
|
|
6207
6210
|
}
|
|
6208
6211
|
};
|
|
6209
|
-
/** The
|
|
6212
|
+
/** The API error code, or the HTTP status printed by git or the API program. */
|
|
6210
6213
|
function accessStatus(error) {
|
|
6211
6214
|
if (error instanceof CliError && error.code !== void 0 && error.code >= 100 && error.code <= 599) return error.code;
|
|
6212
6215
|
const found = /\b(?:HTTP|returned error:)\s*(\d{3})\b/i.exec(error instanceof Error ? error.message : String(error));
|
|
6213
6216
|
return found ? Number(found[1]) : void 0;
|
|
6214
6217
|
}
|
|
6215
6218
|
const PUSH_BACKOFF_MS = [2e3, 5e3];
|
|
6216
|
-
/** Anything that is not a non-fast-forward or an access failure is transient
|
|
6219
|
+
/** Anything that is not a non-fast-forward or an access failure is transient. */
|
|
6217
6220
|
function pushFailure(message, status) {
|
|
6218
6221
|
if (/non-fast-forward|fetch first|\[rejected\]|updates were rejected/i.test(message)) return { kind: "rejected" };
|
|
6219
6222
|
if (status === 401 || status === 403 || status === 404) return {
|
|
@@ -6547,12 +6550,8 @@ var ScratchWorkspaces = class {
|
|
|
6547
6550
|
}
|
|
6548
6551
|
};
|
|
6549
6552
|
// src/infra/health.ts
|
|
6550
|
-
/** Minimum time between two health snapshot writes by a HealthRecorder. */
|
|
6551
6553
|
const HEALTH_SNAPSHOT_MS = 5e3;
|
|
6552
|
-
/**
|
|
6553
|
-
* Central registry of component health entries, enforcing single-writer ownership per entry name.
|
|
6554
|
-
* Claimed by the Runtime, Effects, WorkerExecutor, RouterHost and Sources, and read by the status CLI.
|
|
6555
|
-
*/
|
|
6554
|
+
/** Health entries, each with one writer: `claim` throws when the name is already held. */
|
|
6556
6555
|
var HealthBoard = class {
|
|
6557
6556
|
entries = {};
|
|
6558
6557
|
claimed = /* @__PURE__ */ new Set();
|
|
@@ -6598,7 +6597,6 @@ var HealthRecorder = class {
|
|
|
6598
6597
|
this.now = now;
|
|
6599
6598
|
this.intervalMs = intervalMs;
|
|
6600
6599
|
}
|
|
6601
|
-
/** Returns true when a snapshot was written. */
|
|
6602
6600
|
record(force = false) {
|
|
6603
6601
|
const t = this.now();
|
|
6604
6602
|
if (!force && this.last !== void 0 && t - this.last < this.intervalMs) return false;
|
|
@@ -6609,7 +6607,10 @@ var HealthRecorder = class {
|
|
|
6609
6607
|
};
|
|
6610
6608
|
// src/sources/admission-filter.ts
|
|
6611
6609
|
/**
|
|
6612
|
-
* AdmissionFilter: the check every Trigger passes before Passes.receive. It admits a Trigger whose actor is on the
|
|
6610
|
+
* AdmissionFilter: the check every Trigger passes before Passes.receive. It admits a Trigger whose actor is on the
|
|
6611
|
+
* allowed list and whose repository is configured, or a Source fact on a Thread of an open ticket whose mandate actor
|
|
6612
|
+
* is allowed. Other Triggers are recorded as rejected and never reach the Engine. It reads only the actor, the human
|
|
6613
|
+
* flag, `meta.repository` and the store, never the text.
|
|
6613
6614
|
*/
|
|
6614
6615
|
var AdmissionFilter = class {
|
|
6615
6616
|
deps;
|
|
@@ -6632,7 +6633,6 @@ var AdmissionFilter = class {
|
|
|
6632
6633
|
if (this.deps.store.rejected.record(entry)) this.deps.rejected?.(entry);
|
|
6633
6634
|
return "filtered";
|
|
6634
6635
|
}
|
|
6635
|
-
/** True when some ref of the Trigger's Threads is subscribed by an open ticket whose mandate actor is allowed. */
|
|
6636
6636
|
followed(trigger, allowed) {
|
|
6637
6637
|
const { store } = this.deps;
|
|
6638
6638
|
return trigger.threads.some((ref) => {
|
|
@@ -6784,9 +6784,10 @@ var SourceRegistry = class {
|
|
|
6784
6784
|
};
|
|
6785
6785
|
// src/app/outbox.ts
|
|
6786
6786
|
/**
|
|
6787
|
-
* Persists outbound channel messages as receipts and delivers them
|
|
6788
|
-
*
|
|
6789
|
-
* Messages wait while processing is paused or their source is unavailable, and settle as dropped
|
|
6787
|
+
* Persists outbound channel messages as receipts and delivers them through `Effects` and `OutboxChannels`.
|
|
6788
|
+
* Each ticket keeps per-conversation ordering until an earlier message fails repeatedly.
|
|
6789
|
+
* Messages wait while processing is paused or their source is unavailable, and settle as dropped
|
|
6790
|
+
* when the target conversation is ignored or gone.
|
|
6790
6791
|
*/
|
|
6791
6792
|
var Outbox = class {
|
|
6792
6793
|
host;
|
|
@@ -6797,7 +6798,7 @@ var Outbox = class {
|
|
|
6797
6798
|
retry: [5e3, 3e5]
|
|
6798
6799
|
});
|
|
6799
6800
|
}
|
|
6800
|
-
/**
|
|
6801
|
+
/** Records a message receipt and enqueues its delivery effect atomically; duplicate message IDs are ignored. */
|
|
6801
6802
|
send(message) {
|
|
6802
6803
|
const { store, effects } = this.host;
|
|
6803
6804
|
store.atomic(() => {
|
|
@@ -6816,7 +6817,12 @@ var Outbox = class {
|
|
|
6816
6817
|
sent() {
|
|
6817
6818
|
return this.host.effects.idle("reply");
|
|
6818
6819
|
}
|
|
6819
|
-
/**
|
|
6820
|
+
/**
|
|
6821
|
+
* Crash rule: the reply effect row is deleted only after the receipt settles. A process stopped after the
|
|
6822
|
+
* Source accepted the message but before `settle` runs sends it again on restart, so each channel must find
|
|
6823
|
+
* a message it already sent by its id and return `sent` without sending again. A stop before the send
|
|
6824
|
+
* sends it once after restart.
|
|
6825
|
+
*/
|
|
6820
6826
|
async deliver(id) {
|
|
6821
6827
|
const { store, channels } = this.host;
|
|
6822
6828
|
const receipt = store.receipts.get(id);
|
|
@@ -6841,9 +6847,8 @@ const systemTimers = {
|
|
|
6841
6847
|
}
|
|
6842
6848
|
};
|
|
6843
6849
|
/**
|
|
6844
|
-
*
|
|
6845
|
-
*
|
|
6846
|
-
* Updates the runtime `HealthEntry` when a pass fails and clears the error once a subsequent pass succeeds.
|
|
6850
|
+
* Serializes passes; sources and tools request immediate passes. A failed pass sets the health entry to error,
|
|
6851
|
+
* and the next successful pass clears it.
|
|
6847
6852
|
*/
|
|
6848
6853
|
var Passes = class {
|
|
6849
6854
|
timer;
|
|
@@ -6918,7 +6923,10 @@ var Passes = class {
|
|
|
6918
6923
|
this.host.afterPass?.();
|
|
6919
6924
|
}
|
|
6920
6925
|
};
|
|
6921
|
-
/**
|
|
6926
|
+
/**
|
|
6927
|
+
* True when the ticket ended at a parseable time at least `days` days before `now`.
|
|
6928
|
+
* A ticket with no ended time, or one that does not parse, never expires.
|
|
6929
|
+
*/
|
|
6922
6930
|
function expired(ticket, days, now) {
|
|
6923
6931
|
const at = ticket.ended ? Date.parse(ticket.ended.at) : NaN;
|
|
6924
6932
|
return Number.isFinite(at) && now - at >= days * 864e5;
|
|
@@ -6931,7 +6939,10 @@ var Retention = class {
|
|
|
6931
6939
|
this.now = host.now ?? (() => Date.now());
|
|
6932
6940
|
host.effects.handle("cleanup", (payload) => this.cleanup(payload.ticketId));
|
|
6933
6941
|
}
|
|
6934
|
-
/**
|
|
6942
|
+
/**
|
|
6943
|
+
* Queues a cleanup for each expired ended ticket that still has a worker record, unless one is already waiting.
|
|
6944
|
+
* Returns how many were queued.
|
|
6945
|
+
*/
|
|
6935
6946
|
evaluate() {
|
|
6936
6947
|
const rows = this.host.store.db.prepare("SELECT w.ticket_id AS id, t.value ->> '$.ended.at' AS ended FROM worker w JOIN ticket t ON t.id = w.ticket_id WHERE t.open IS NULL").all();
|
|
6937
6948
|
let queued = 0;
|
|
@@ -6945,7 +6956,10 @@ var Retention = class {
|
|
|
6945
6956
|
}
|
|
6946
6957
|
return queued;
|
|
6947
6958
|
}
|
|
6948
|
-
/**
|
|
6959
|
+
/**
|
|
6960
|
+
* Removes the worker record, then the workspace, of an expired ended ticket.
|
|
6961
|
+
* A ticket that is open again, unknown, or not expired is left alone. Safe to repeat.
|
|
6962
|
+
*/
|
|
6949
6963
|
async cleanup(ticketId) {
|
|
6950
6964
|
const ticket = this.host.store.tickets.get(ticketId);
|
|
6951
6965
|
if (!ticket || !expired(ticket, this.host.days(), this.now())) return;
|
|
@@ -6955,10 +6969,7 @@ var Retention = class {
|
|
|
6955
6969
|
}
|
|
6956
6970
|
};
|
|
6957
6971
|
// src/app/store-read.ts
|
|
6958
|
-
/**
|
|
6959
|
-
* Read-only Store queries for views, tools, and Source polling.
|
|
6960
|
-
* Changes from the reference: `openReadOnly` (with `keys`) and `writerActive` are added for the CLI.
|
|
6961
|
-
*/
|
|
6972
|
+
/** `openReadOnly` and `writerActive` serve the CLI. */
|
|
6962
6973
|
function storeReads(db) {
|
|
6963
6974
|
const values = (sql, ...parameters) => db.prepare(sql).all(...parameters).map((row) => JSON.parse(String(row.value)));
|
|
6964
6975
|
return {
|
|
@@ -7064,7 +7075,10 @@ function openReadOnly(path) {
|
|
|
7064
7075
|
}
|
|
7065
7076
|
};
|
|
7066
7077
|
}
|
|
7067
|
-
/**
|
|
7078
|
+
/**
|
|
7079
|
+
* Whether a writer (the daemon) holds the single-writer lock on the database at `path`. Takes no lock itself.
|
|
7080
|
+
* A lock older than the Store's stale time counts as not held, and so does a lock whose recorded owner is gone.
|
|
7081
|
+
*/
|
|
7068
7082
|
function writerActive(path) {
|
|
7069
7083
|
try {
|
|
7070
7084
|
return checkSync(path, {
|
|
@@ -7078,12 +7092,9 @@ function writerActive(path) {
|
|
|
7078
7092
|
// src/app/store.ts
|
|
7079
7093
|
/**
|
|
7080
7094
|
* SQLite persistence for domain aggregates, key-value state, and the channel outbox.
|
|
7081
|
-
*
|
|
7082
|
-
*/
|
|
7083
|
-
/**
|
|
7084
|
-
* Generated columns and partial indexes defining active working sets so passes scan only active rows.
|
|
7085
|
-
* `schemaSql` declares them in the version 1 tables, and SQLite computes them from the stored JSON rows.
|
|
7095
|
+
* The Store holds a single-writer lock on the database file.
|
|
7086
7096
|
*/
|
|
7097
|
+
/** Generated columns and partial indexes for active working sets, computed by SQLite from the stored JSON rows. */
|
|
7087
7098
|
const SETS = {
|
|
7088
7099
|
ticket: {
|
|
7089
7100
|
open: "INTEGER AS (CASE WHEN value -> '$.ended' IS NULL THEN 1 END)",
|
|
@@ -7100,11 +7111,8 @@ const SETS = {
|
|
|
7100
7111
|
AND value ->> '$.ticketId' IS NOT NULL THEN (value ->> '$.ticketId') || ' ' || (value ->> '$.channel.thread') END)`
|
|
7101
7112
|
}
|
|
7102
7113
|
};
|
|
7103
|
-
/** File name of the SQLite database inside the state directory. */
|
|
7104
7114
|
const DATABASE_FILE = "superagent-claude.db";
|
|
7105
|
-
/** Path of the SQLite database file inside `stateDir`. */
|
|
7106
7115
|
const databasePath = (stateDir) => join(stateDir, DATABASE_FILE);
|
|
7107
|
-
/** Table, column and index DDL for schema version 1, with the working-set columns and indexes from `SETS`. */
|
|
7108
7116
|
function schemaSql() {
|
|
7109
7117
|
const extra = (table) => Object.entries(SETS[table] ?? {}).map(([name, definition]) => `, ${name} ${definition}`).join("");
|
|
7110
7118
|
const sets = Object.entries(SETS).flatMap(([table, definitions]) => Object.keys(definitions).map((name) => `CREATE INDEX IF NOT EXISTS ${table}_${name} ON ${table}(${name}) WHERE ${name} IS NOT NULL;`));
|
|
@@ -7130,7 +7138,6 @@ function schemaSql() {
|
|
|
7130
7138
|
${sets.join("\n ")}
|
|
7131
7139
|
`;
|
|
7132
7140
|
}
|
|
7133
|
-
/** Maps a `rejected_trigger` row to its RejectedTrigger. */
|
|
7134
7141
|
function toRejected(row) {
|
|
7135
7142
|
return {
|
|
7136
7143
|
triggerId: String(row.trigger_id),
|
|
@@ -7142,7 +7149,10 @@ function toRejected(row) {
|
|
|
7142
7149
|
}
|
|
7143
7150
|
/** The file next to the database that names the pid holding its single-writer lock. */
|
|
7144
7151
|
const ownerFile = (path) => `${path}.owner`;
|
|
7145
|
-
/**
|
|
7152
|
+
/**
|
|
7153
|
+
* The recorded lock owner, or undefined when the file is missing or names no positive pid.
|
|
7154
|
+
* An owner without a host is checked as a local process.
|
|
7155
|
+
*/
|
|
7146
7156
|
function lockOwner(path) {
|
|
7147
7157
|
let text;
|
|
7148
7158
|
try {
|
|
@@ -7159,7 +7169,8 @@ function lockOwner(path) {
|
|
|
7159
7169
|
} : { pid };
|
|
7160
7170
|
}
|
|
7161
7171
|
/**
|
|
7162
|
-
* True when the lock on `path` names an owner pid that no longer runs on this host
|
|
7172
|
+
* True when the lock on `path` names an owner pid that no longer runs on this host,
|
|
7173
|
+
* i.e. the owner was killed and left its lock behind.
|
|
7163
7174
|
* A lock recorded on another host is never taken over at once.
|
|
7164
7175
|
*/
|
|
7165
7176
|
function lockOwnerGone(path) {
|
|
@@ -7173,10 +7184,7 @@ function lockOwnerGone(path) {
|
|
|
7173
7184
|
return error.code === "ESRCH";
|
|
7174
7185
|
}
|
|
7175
7186
|
}
|
|
7176
|
-
/**
|
|
7177
|
-
* Takes the single-writer lock on `path`; a lock held by another process is reported as a clear error. Returns the release function.
|
|
7178
|
-
* A lock left by a killed process is taken over at once when its recorded owner pid no longer runs; otherwise the 30 s stale rule applies.
|
|
7179
|
-
*/
|
|
7187
|
+
/** A lock whose recorded owner pid no longer runs is taken over at once; otherwise the 30 s stale rule applies. */
|
|
7180
7188
|
function lockDatabase(path, report) {
|
|
7181
7189
|
const take = () => lockSync(path, {
|
|
7182
7190
|
realpath: false,
|
|
@@ -7215,18 +7223,20 @@ function lockDatabase(path, report) {
|
|
|
7215
7223
|
};
|
|
7216
7224
|
}
|
|
7217
7225
|
/**
|
|
7218
|
-
*
|
|
7219
|
-
*
|
|
7226
|
+
* Opening takes a single-writer lock on the database file.
|
|
7227
|
+
* A database stored at any version other than `SCHEMA_VERSION` is refused.
|
|
7220
7228
|
*/
|
|
7221
7229
|
var Store = class {
|
|
7222
7230
|
db;
|
|
7223
7231
|
/** Receives errors thrown by `afterCommit` callbacks and lock release without failing the committed transaction. */
|
|
7224
7232
|
report;
|
|
7225
|
-
/** Releases the single-writer lock on the database file. */
|
|
7226
7233
|
unlock;
|
|
7227
7234
|
/**
|
|
7228
|
-
* Opens or creates the SQLite database at `path` under the single-writer lock.
|
|
7229
|
-
*
|
|
7235
|
+
* Opens or creates the SQLite database at `path` under the single-writer lock.
|
|
7236
|
+
* Waits up to 5 s for a lock another connection holds (such as a reader during crash recovery).
|
|
7237
|
+
* @throws When another process holds the lock, when the stored schema is not `SCHEMA_VERSION`,
|
|
7238
|
+
* or when the database cannot be opened. In every case the lock is released and the SQLite handle
|
|
7239
|
+
* is closed so a later attempt can open it.
|
|
7230
7240
|
*/
|
|
7231
7241
|
constructor(path, report = () => {}) {
|
|
7232
7242
|
this.report = report;
|
|
@@ -7257,12 +7267,10 @@ var Store = class {
|
|
|
7257
7267
|
throw error;
|
|
7258
7268
|
}
|
|
7259
7269
|
}
|
|
7260
|
-
/** Closes the underlying SQLite database connection and releases the single-writer lock. */
|
|
7261
7270
|
close() {
|
|
7262
7271
|
this.db.close();
|
|
7263
7272
|
this.unlock();
|
|
7264
7273
|
}
|
|
7265
|
-
/** Parses JSON `value` columns returned by `sql`. */
|
|
7266
7274
|
values(sql, ...parameters) {
|
|
7267
7275
|
return this.db.prepare(sql).all(...parameters).map((row) => JSON.parse(String(row.value)));
|
|
7268
7276
|
}
|
|
@@ -7273,11 +7281,13 @@ var Store = class {
|
|
|
7273
7281
|
for (const ref of trigger.threads) insert.run(threadKey(ref), trigger.id, trigger.at, trigger.human ? 1 : 0);
|
|
7274
7282
|
}
|
|
7275
7283
|
depth = 0;
|
|
7276
|
-
/**
|
|
7284
|
+
/** Post-commit callbacks, indexed by savepoint level; rolling back a level discards its callbacks. */
|
|
7277
7285
|
committed = [];
|
|
7278
7286
|
/**
|
|
7279
|
-
* Executes `operation` in a savepoint transaction (nested inside the current
|
|
7280
|
-
*
|
|
7287
|
+
* Executes `operation` in a savepoint transaction (nested inside the current one if any);
|
|
7288
|
+
* a failure rolls back its changes.
|
|
7289
|
+
* Post-commit callbacks run only after the outermost transaction commits. Callback errors are reported
|
|
7290
|
+
* without failing the committed transaction.
|
|
7281
7291
|
*/
|
|
7282
7292
|
atomic(operation) {
|
|
7283
7293
|
const level = this.depth++;
|
|
@@ -7305,24 +7315,22 @@ var Store = class {
|
|
|
7305
7315
|
return value;
|
|
7306
7316
|
}
|
|
7307
7317
|
/**
|
|
7308
|
-
* Schedules `action` to run once the outermost transaction commits, or immediately if outside a transaction
|
|
7309
|
-
*
|
|
7310
|
-
* Use `afterCommit` for
|
|
7318
|
+
* Schedules `action` to run once the outermost transaction commits, or immediately if outside a transaction.
|
|
7319
|
+
* Callbacks queued inside a transaction that rolls back are discarded and never run.
|
|
7320
|
+
* Use `afterCommit` only for in-memory follow-ups that need not survive a restart;
|
|
7321
|
+
* use `Effects` for durable side effects.
|
|
7311
7322
|
*/
|
|
7312
7323
|
afterCommit(action) {
|
|
7313
7324
|
if (this.depth) this.committed[this.depth - 1].push(action);
|
|
7314
7325
|
else action();
|
|
7315
7326
|
}
|
|
7316
|
-
/** Reads a typed KV record by its key in `KV`. */
|
|
7317
7327
|
get(name) {
|
|
7318
7328
|
const row = this.db.prepare("SELECT value FROM kv WHERE key=?").get(name);
|
|
7319
7329
|
return row === void 0 ? void 0 : JSON.parse(String(row.value));
|
|
7320
7330
|
}
|
|
7321
|
-
/** Writes a typed KV record under `name`. */
|
|
7322
7331
|
set(name, value) {
|
|
7323
7332
|
this.db.prepare("INSERT INTO kv VALUES (?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(name, JSON.stringify(value));
|
|
7324
7333
|
}
|
|
7325
|
-
/** Deletes the KV record at `name`. */
|
|
7326
7334
|
delete(name) {
|
|
7327
7335
|
this.db.prepare("DELETE FROM kv WHERE key=?").run(name);
|
|
7328
7336
|
}
|
|
@@ -7357,7 +7365,10 @@ var Store = class {
|
|
|
7357
7365
|
},
|
|
7358
7366
|
list: () => this.db.prepare("SELECT value FROM thread ORDER BY rowid").all().map((row) => JSON.parse(String(row.value))),
|
|
7359
7367
|
active: () => this.values("SELECT value FROM thread WHERE active = 1 ORDER BY rowid"),
|
|
7360
|
-
/**
|
|
7368
|
+
/**
|
|
7369
|
+
* Updates human-facing thread metadata. `null` in `facts` removes that fact by ID.
|
|
7370
|
+
* No-op if the thread does not exist.
|
|
7371
|
+
*/
|
|
7361
7372
|
describe: (keyValue, patch) => {
|
|
7362
7373
|
const thread = this.threads.get(keyValue);
|
|
7363
7374
|
if (!thread) return;
|
|
@@ -7375,7 +7386,10 @@ var Store = class {
|
|
|
7375
7386
|
meta
|
|
7376
7387
|
});
|
|
7377
7388
|
},
|
|
7378
|
-
/**
|
|
7389
|
+
/**
|
|
7390
|
+
* Atomically sets or removes (`undefined`) source-specific metadata keys after re-reading the thread.
|
|
7391
|
+
* No-op if the thread does not exist.
|
|
7392
|
+
*/
|
|
7379
7393
|
patchMeta: (keyValue, patch) => this.atomic(() => {
|
|
7380
7394
|
const thread = this.threads.get(keyValue);
|
|
7381
7395
|
if (!thread) return;
|
|
@@ -7434,7 +7448,10 @@ var Store = class {
|
|
|
7434
7448
|
get: (id) => this.values("SELECT value FROM outbox WHERE id = ?", id)[0],
|
|
7435
7449
|
/** Pending message receipts, oldest first. */
|
|
7436
7450
|
waiting: () => this.values("SELECT value FROM outbox WHERE value ->> '$.status' = 'pending' ORDER BY rowid"),
|
|
7437
|
-
/**
|
|
7451
|
+
/**
|
|
7452
|
+
* True if an earlier message for the same ticket and conversation is still pending,
|
|
7453
|
+
* with fewer than `failures` failed `reply` effect attempts.
|
|
7454
|
+
*/
|
|
7438
7455
|
behind: (id, failures) => !!this.db.prepare(`SELECT 1 FROM outbox earlier
|
|
7439
7456
|
JOIN outbox message ON message.id = ? LEFT JOIN effect ON effect.kind = 'reply' AND effect.key = earlier.id
|
|
7440
7457
|
WHERE earlier.pending = message.pending AND earlier.rowid < message.rowid AND IFNULL(effect.attempts, 0) < ? LIMIT 1`).get(id, failures),
|
|
@@ -7450,8 +7467,8 @@ var Store = class {
|
|
|
7450
7467
|
return Number(this.db.prepare("INSERT OR IGNORE INTO outbox (id, value) VALUES (?, ?)").run(message.id, JSON.stringify(receipt)).changes) > 0;
|
|
7451
7468
|
},
|
|
7452
7469
|
/**
|
|
7453
|
-
* Marks a pending receipt as sent or dropped (with drop reason and timestamp)
|
|
7454
|
-
* was withdrawn while being sent so its ID cannot be queued again.
|
|
7470
|
+
* Marks a pending receipt as sent or dropped (with drop reason and timestamp).
|
|
7471
|
+
* Recreates the receipt if the message was withdrawn while being sent, so its ID cannot be queued again.
|
|
7455
7472
|
*/
|
|
7456
7473
|
settle: (message, outcome) => {
|
|
7457
7474
|
const settled = outcome === "sent" ? { status: "sent" } : {
|
|
@@ -7480,10 +7497,6 @@ var Store = class {
|
|
|
7480
7497
|
};
|
|
7481
7498
|
};
|
|
7482
7499
|
// src/infra/paths.ts
|
|
7483
|
-
/**
|
|
7484
|
-
* Resolves packaged asset paths from either the source tree or the compiled lib/ output.
|
|
7485
|
-
* Changes from the reference: the package root is found by the name `superagent-claude`.
|
|
7486
|
-
*/
|
|
7487
7500
|
const PACKAGE_NAME = "superagent-claude";
|
|
7488
7501
|
let cached;
|
|
7489
7502
|
function packageRoot() {
|
|
@@ -7499,7 +7512,11 @@ function packageRoot() {
|
|
|
7499
7512
|
throw new Error(`${PACKAGE_NAME} package root not found`);
|
|
7500
7513
|
}
|
|
7501
7514
|
// src/app/prompts.ts
|
|
7502
|
-
/**
|
|
7515
|
+
/**
|
|
7516
|
+
* Role prompts assembled from host rules, packaged built-in parts, policy rules, the team's three sections,
|
|
7517
|
+
* and wrapped external text. Parts are ordered host > built-in > rule > team > external data.
|
|
7518
|
+
* Versions are immutable content hashes stored in the prompt_version table.
|
|
7519
|
+
*/
|
|
7503
7520
|
const TEAM_SECTIONS = [
|
|
7504
7521
|
"communication",
|
|
7505
7522
|
"scope",
|
|
@@ -7845,7 +7862,11 @@ function noticeText(previous, next) {
|
|
|
7845
7862
|
if (!texts.length) return void 0;
|
|
7846
7863
|
return NOTICE_HEADER + texts.join("\n\n");
|
|
7847
7864
|
}
|
|
7848
|
-
/**
|
|
7865
|
+
/**
|
|
7866
|
+
* Classifies the change from a stored prompt version to the current assembly: `fixed` when the role or a host,
|
|
7867
|
+
* built-in or skill part changed (a new session is needed); `team` when only rule or team sections changed
|
|
7868
|
+
* (a notice is enough); `same` otherwise.
|
|
7869
|
+
*/
|
|
7849
7870
|
function promptChange(stored, current) {
|
|
7850
7871
|
if (!stored || stored.role !== current.role) return { kind: "fixed" };
|
|
7851
7872
|
const now = [...current.parts.filter((p) => p.layer === "host" || p.layer === "builtin").map((p) => [p.key, partHash(p.text)]), ["skill/github", current.skillHash]];
|
|
@@ -7915,11 +7936,8 @@ var Prompts = class {
|
|
|
7915
7936
|
};
|
|
7916
7937
|
// src/app/cards.ts
|
|
7917
7938
|
/**
|
|
7918
|
-
*
|
|
7919
|
-
*
|
|
7920
|
-
* link are removed (M16 publishes pull requests); the input and batch resource links are removed, so `inputLine`
|
|
7921
|
-
* names a trigger, a decision (for the Router only) or a short text, and `ticketDetails` drops the batch, transcript,
|
|
7922
|
-
* assignment and result links, carrying the latest report's summary instead; `threadCard` labels a thread by its metadata.
|
|
7939
|
+
* Full texts stay behind readable resources. Every trigger text a card carries is wrapped by `external()`
|
|
7940
|
+
* as untrusted data.
|
|
7923
7941
|
*/
|
|
7924
7942
|
/** Wraps untrusted text from a source as external data, so the model reads it as data rather than as instructions. */
|
|
7925
7943
|
function external(source, text) {
|
|
@@ -7928,7 +7946,10 @@ function external(source, text) {
|
|
|
7928
7946
|
text
|
|
7929
7947
|
});
|
|
7930
7948
|
}
|
|
7931
|
-
/**
|
|
7949
|
+
/**
|
|
7950
|
+
* Collapses whitespace and truncates text for display previews, keeping code, angle brackets, and links intact
|
|
7951
|
+
* for model readers.
|
|
7952
|
+
*/
|
|
7932
7953
|
function preview(text) {
|
|
7933
7954
|
return text.replace(/\s+/g, " ").slice(0, 160);
|
|
7934
7955
|
}
|
|
@@ -7979,7 +8000,7 @@ function ticketCard(ticket) {
|
|
|
7979
8000
|
};
|
|
7980
8001
|
}
|
|
7981
8002
|
const INPUT_TEXT = 80;
|
|
7982
|
-
/**
|
|
8003
|
+
/** A decision shows for the Router only; a trigger shows by its ID. */
|
|
7983
8004
|
function inputLine(ticket, input, worker = false) {
|
|
7984
8005
|
const names = input.kind === "trigger" || input.kind === "resumed" ? { trigger_id: input.triggerId } : input.kind === "reopen" && !worker ? { decision_id: input.decisionId } : input.kind !== "text" ? {} : input.text.length <= INPUT_TEXT ? { text: input.text } : {
|
|
7985
8006
|
preview: preview(input.text).slice(0, INPUT_TEXT),
|
|
@@ -8017,7 +8038,7 @@ function newestPage(items, offset, size, line, next) {
|
|
|
8017
8038
|
} : {}
|
|
8018
8039
|
};
|
|
8019
8040
|
}
|
|
8020
|
-
/**
|
|
8041
|
+
/** Workers get no decision IDs. */
|
|
8021
8042
|
function ticketDetails(ticket, threads, worker = false) {
|
|
8022
8043
|
const { preview: _preview, ...card } = ticketCard(ticket);
|
|
8023
8044
|
return {
|
|
@@ -8081,11 +8102,11 @@ function relatedTickets(tickets) {
|
|
|
8081
8102
|
}
|
|
8082
8103
|
// src/app/resources.ts
|
|
8083
8104
|
/**
|
|
8084
|
-
* Paginated resources that orchestrator_read serves to the Router session and to a ticket's worker,
|
|
8085
|
-
* model prompts until they are requested.
|
|
8086
|
-
*
|
|
8087
|
-
*
|
|
8088
|
-
*
|
|
8105
|
+
* Paginated resources that orchestrator_read serves to the Router session and to a ticket's worker,
|
|
8106
|
+
* so large bodies stay out of model prompts until they are requested.
|
|
8107
|
+
* The Router session sees `tickets`; the worker sees its own ticket only. `github/…` reads go through the
|
|
8108
|
+
* configured GitHub reader, with the repository checked against the configured list.
|
|
8109
|
+
* Text bodies of triggers and GitHub originals are wrapped as external data.
|
|
8089
8110
|
*/
|
|
8090
8111
|
/** Resource descriptors exposed via orchestrator_read; the worker reads only its own ticket and what reached it. */
|
|
8091
8112
|
const RESOURCES = [
|
|
@@ -8130,7 +8151,7 @@ function list(offset, limit, fetch) {
|
|
|
8130
8151
|
next_offset: items.length > size ? offset + size : null
|
|
8131
8152
|
};
|
|
8132
8153
|
}
|
|
8133
|
-
/** Whether a worker's ticket may read a trigger: it reached the ticket, started it, or is on a thread
|
|
8154
|
+
/** Whether a worker's ticket may read a trigger: it reached the ticket, started it, or is on a followed thread. */
|
|
8134
8155
|
const readable = (host, ticket, found) => reached(ticket, found, (key) => host.store.threads.get(key)) || ticket.origin.triggerIds.includes(found.id) || found.threads.some((ref) => ticket.follows?.some((follow) => follow.thread === threadKey(ref)));
|
|
8135
8156
|
function trigger$1(host, id, ticketId) {
|
|
8136
8157
|
const found = host.store.triggers.get(id);
|
|
@@ -8148,7 +8169,10 @@ const triggerLine = (item) => ({
|
|
|
8148
8169
|
chars: item.text.length,
|
|
8149
8170
|
read: `trigger/${item.id}`
|
|
8150
8171
|
});
|
|
8151
|
-
/**
|
|
8172
|
+
/**
|
|
8173
|
+
* The threads a ticket is tied to: those it is subscribed to (context-only ones marked),
|
|
8174
|
+
* and those its own source bound it to.
|
|
8175
|
+
*/
|
|
8152
8176
|
function ticketThreads(host, ticket) {
|
|
8153
8177
|
const bound = new Set((ticket.follows ?? []).map((follow) => follow.thread));
|
|
8154
8178
|
const subscribed = host.store.threads.list().filter((item) => item.subscriptions.includes(ticket.id));
|
|
@@ -8159,7 +8183,10 @@ function ticketThreads(host, ticket) {
|
|
|
8159
8183
|
});
|
|
8160
8184
|
return [...subscribed.map((item) => marked(item.key, !!item.context?.includes(ticket.id))), ...[...bound].filter((thread) => !subscribed.some((item) => item.key === thread)).map((thread) => marked(thread, false))];
|
|
8161
8185
|
}
|
|
8162
|
-
/**
|
|
8186
|
+
/**
|
|
8187
|
+
* Resolves a GitHub resource's repository: explicit in the name, implicit from the ticket (worker),
|
|
8188
|
+
* or the one configured repository (Router).
|
|
8189
|
+
*/
|
|
8163
8190
|
function githubRepository(host, resource, ticketId) {
|
|
8164
8191
|
if (/^github\/(issues|pulls|commits)\//.test(resource)) {
|
|
8165
8192
|
const ticket = ticketId ? host.store.tickets.get(ticketId) : void 0;
|
|
@@ -8178,8 +8205,10 @@ function githubRepository(host, resource, ticketId) {
|
|
|
8178
8205
|
};
|
|
8179
8206
|
}
|
|
8180
8207
|
/**
|
|
8181
|
-
*
|
|
8182
|
-
*
|
|
8208
|
+
* Reads a paginated resource for the Router session (`ticketId` undefined) or a ticket's worker.
|
|
8209
|
+
* Unknown names are tool errors.
|
|
8210
|
+
* @throws {Error} for a resource the caller may not read or that does not exist;
|
|
8211
|
+
* `UserError` for a missing or unreached trigger or ticket.
|
|
8183
8212
|
*/
|
|
8184
8213
|
async function readResource(host, resource, offset, limit, ticketId, signal) {
|
|
8185
8214
|
const worker = ticketId !== void 0;
|
|
@@ -8266,7 +8295,6 @@ async function readResource(host, resource, offset, limit, ticketId, signal) {
|
|
|
8266
8295
|
if (resource === "skill/github") return text(resource, host.skill(), offset, limit);
|
|
8267
8296
|
throw new Error(`Unknown resource; use one of: ${resourceGrammar(worker)}`);
|
|
8268
8297
|
}
|
|
8269
|
-
/** Formats a trigger for a worker or the Router: author, thread, timestamp, text cut at TRIGGER_CHARS, and the message it replies to. */
|
|
8270
8298
|
function triggerMessage(trigger, note, repliesTo) {
|
|
8271
8299
|
const cut = trigger.text.length > 600 ? `${trigger.text.slice(0, 600)}…` : void 0;
|
|
8272
8300
|
return {
|
|
@@ -8292,14 +8320,13 @@ function repliedTo(host, trigger) {
|
|
|
8292
8320
|
const parent = host.sources.repliesTo(trigger);
|
|
8293
8321
|
return parent === void 0 ? void 0 : host.store.triggers.get(parent);
|
|
8294
8322
|
}
|
|
8295
|
-
/** Finds tickets subscribed to threads
|
|
8323
|
+
/** Finds tickets subscribed to threads that `trigger.text` references (such as `#12` or URLs), excluding `known`. */
|
|
8296
8324
|
function referencedTickets(host, trigger, known) {
|
|
8297
8325
|
const threads = (trigger.text.match(/#\d+|https?:\/\/[^\s)>\]]+/g) ?? []).flatMap((reference) => host.sources.find(reference).threads);
|
|
8298
8326
|
const ids = new Set(threads.flatMap((thread) => host.store.threads.get(thread)?.subscriptions ?? []));
|
|
8299
8327
|
for (const ticket of known) ids.delete(ticket.id);
|
|
8300
8328
|
return [...ids].flatMap((id) => host.store.tickets.get(id) ?? []).slice(0, 4);
|
|
8301
8329
|
}
|
|
8302
|
-
/** Builds the Router's candidate card with routing metadata, handback history, and related or referenced tickets. */
|
|
8303
8330
|
function candidateCard(host, candidate) {
|
|
8304
8331
|
const primary = candidate.triggers.at(-1);
|
|
8305
8332
|
const delivered = host.store.triggers.disposition(primary.id)?.fanout ?? [];
|
|
@@ -8323,25 +8350,21 @@ function candidateCard(host, candidate) {
|
|
|
8323
8350
|
};
|
|
8324
8351
|
}
|
|
8325
8352
|
// src/app/notices.ts
|
|
8326
|
-
/**
|
|
8327
|
-
* Formats background notices and steering relays delivered to the Router session through the effect queue.
|
|
8328
|
-
* Changes from the reference: the prompt-configuration and settings notices, and `changedPaths`, are removed (ADR-9); the
|
|
8329
|
-
* thread-followed notice is added (R18); a follow-up relay reads the trigger through `triggerMessage`.
|
|
8330
|
-
*/
|
|
8331
8353
|
/** Strips the latest report preview from a ticket card when the relay already carries or does not need the summary. */
|
|
8332
8354
|
function brief(ticket) {
|
|
8333
8355
|
const { preview: _preview, ...card } = ticketCard(ticket);
|
|
8334
8356
|
return card;
|
|
8335
8357
|
}
|
|
8336
|
-
/** Creates a one-off receipt notice with a unique `name` key so
|
|
8358
|
+
/** Creates a one-off receipt notice with a unique `name` key, so each occurrence is delivered instead of coalescing. */
|
|
8337
8359
|
const receiptNotice = (value, summary) => [
|
|
8338
8360
|
key("receipt", randomUUID()),
|
|
8339
8361
|
value,
|
|
8340
8362
|
summary
|
|
8341
8363
|
];
|
|
8342
8364
|
/**
|
|
8343
|
-
* Builds a notice that a person removed a thread's subscriptions at `at
|
|
8344
|
-
*
|
|
8365
|
+
* Builds a notice that a person removed a thread's subscriptions at `at`.
|
|
8366
|
+
* A later removal of the same thread replaces an unsent waiting notice.
|
|
8367
|
+
* The thread key is `<source>:<id>`, such as `github:<owner>/<name>#<n>`.
|
|
8345
8368
|
*/
|
|
8346
8369
|
const subscriptionNotice = (thread, at) => [
|
|
8347
8370
|
`subscription:${thread}`,
|
|
@@ -8355,8 +8378,8 @@ const subscriptionNotice = (thread, at) => [
|
|
|
8355
8378
|
`Removed the subscriptions on thread ${thread}`
|
|
8356
8379
|
];
|
|
8357
8380
|
/**
|
|
8358
|
-
* Converts an unsolicited domain event into a Router notice (`ticket.ended`, `trigger.requeued`, or
|
|
8359
|
-
* by a person or executor)
|
|
8381
|
+
* Converts an unsolicited domain event into a Router notice (`ticket.ended`, `trigger.requeued`, or
|
|
8382
|
+
* `thread.unsubscribed` by a person or executor). Returns `undefined` for events that need no notice.
|
|
8360
8383
|
*/
|
|
8361
8384
|
function eventNotice(event, ticket = () => void 0) {
|
|
8362
8385
|
const { at } = event;
|
|
@@ -8402,12 +8425,16 @@ const threadFollowedNotice = (ticketId, thread, reason) => receiptNotice({
|
|
|
8402
8425
|
},
|
|
8403
8426
|
note: "For context only: its messages reach this ticket and grant nothing."
|
|
8404
8427
|
}, `Ticket ${ticketId} follows thread ${thread} for context: ${reason}`);
|
|
8405
|
-
/**
|
|
8428
|
+
/**
|
|
8429
|
+
* What a follow-up asks of the Router session: only a stop that the person asks for,
|
|
8430
|
+
* since its ticket's worker decides it.
|
|
8431
|
+
*/
|
|
8406
8432
|
const FOLLOWUP_NOTE = "Its ticket's worker has it and decides; act only if the person asks to stop the work (orchestrator_route kind stop with this trigger_id).";
|
|
8407
8433
|
/**
|
|
8408
|
-
* Builds a steering relay for a human follow-up trigger that reached at least one live ticket
|
|
8409
|
-
* that resumed on the follow-up)
|
|
8410
|
-
*
|
|
8434
|
+
* Builds a steering relay for a human follow-up trigger that reached at least one live ticket
|
|
8435
|
+
* (including an ended ticket that resumed on the follow-up). The trigger is rendered as workers read it,
|
|
8436
|
+
* with the replied-to message and the open tickets it reached.
|
|
8437
|
+
* Returns `undefined` while no ticket's worker decides it (see `followupOwners`) or it reached no live ticket.
|
|
8411
8438
|
*/
|
|
8412
8439
|
function followupNotice(store, sources, triggerId) {
|
|
8413
8440
|
const trigger = store.triggers.get(triggerId);
|
|
@@ -8430,8 +8457,9 @@ function followupNotice(store, sources, triggerId) {
|
|
|
8430
8457
|
}, trigger)), tickets);
|
|
8431
8458
|
}
|
|
8432
8459
|
/**
|
|
8433
|
-
* A person's message that reached tickets, as the Router session hears of it: the message as their workers
|
|
8434
|
-
* tickets it reached with their state. Their workers decide it; the Router session may only
|
|
8460
|
+
* A person's message that reached tickets, as the Router session hears of it: the message as their workers
|
|
8461
|
+
* read it, and the tickets it reached with their state. Their workers decide it; the Router session may only
|
|
8462
|
+
* stop the work on it.
|
|
8435
8463
|
*/
|
|
8436
8464
|
function followupRelay(message, tickets) {
|
|
8437
8465
|
return {
|
|
@@ -8444,7 +8472,11 @@ function followupRelay(message, tickets) {
|
|
|
8444
8472
|
})
|
|
8445
8473
|
};
|
|
8446
8474
|
}
|
|
8447
|
-
/**
|
|
8475
|
+
/**
|
|
8476
|
+
* Formats a worker report relay for the Router session.
|
|
8477
|
+
* Summaries over 600 characters are truncated behind `ticket/<id>`.
|
|
8478
|
+
* `replyTo` names the human question trigger it answers.
|
|
8479
|
+
*/
|
|
8448
8480
|
function resultRelay(ticket, state, text, replyTo) {
|
|
8449
8481
|
const id = key("worker-result", ticket.id, ticket.updatedAt, text);
|
|
8450
8482
|
const summary = text.length > 600 ? {
|
|
@@ -8466,9 +8498,9 @@ function resultRelay(ticket, state, text, replyTo) {
|
|
|
8466
8498
|
};
|
|
8467
8499
|
}
|
|
8468
8500
|
/**
|
|
8469
|
-
* Converts a worker report recorded on `ticket` into a Router relay
|
|
8470
|
-
* relay
|
|
8471
|
-
* reports without `answers` stay internal.
|
|
8501
|
+
* Converts a worker report recorded on `ticket` into a Router relay. A question for a person produces a
|
|
8502
|
+
* `worker_attention` relay; a result, blocker, end, or answer to a question produces a `worker_report` relay.
|
|
8503
|
+
* Routine `progress` reports without `answers` stay internal.
|
|
8472
8504
|
* @returns The steering `Relay`, or `undefined` when the report is not relayed to the Router session.
|
|
8473
8505
|
*/
|
|
8474
8506
|
function reportRelay(ticket, report) {
|
|
@@ -8507,7 +8539,7 @@ const HumanActionSchema = z.string().trim().min(1).max(1e3).superRefine((text, c
|
|
|
8507
8539
|
message: `human_action must be one concrete question for a person: ${problem}`
|
|
8508
8540
|
});
|
|
8509
8541
|
});
|
|
8510
|
-
/** The `orchestrator_read` tool: one page of a resource the grammar lists, read through `read`. */
|
|
8542
|
+
/** The `orchestrator_read` tool: one page of a resource that the grammar lists, read through `read`. */
|
|
8511
8543
|
function readTool(description, grammar, read) {
|
|
8512
8544
|
return tool$1({
|
|
8513
8545
|
name: "orchestrator_read",
|
|
@@ -8521,8 +8553,12 @@ function readTool(description, grammar, read) {
|
|
|
8521
8553
|
});
|
|
8522
8554
|
}
|
|
8523
8555
|
// src/app/tools/delivery.ts
|
|
8524
|
-
/**
|
|
8525
|
-
|
|
8556
|
+
/**
|
|
8557
|
+
* Policy is checked before any git or GitHub call: live ticket, configured repository, a PR this ticket bound,
|
|
8558
|
+
* and delivery.allowAutoMerge. Publish re-checks push permission first and returns `published: false`
|
|
8559
|
+
* without any write when it is missing. Repository access errors file a failed report.
|
|
8560
|
+
*/
|
|
8561
|
+
/** The issue a Trigger is on: its meta names a repository and a number, and `isPr` is false. */
|
|
8526
8562
|
function issueOf(trigger) {
|
|
8527
8563
|
const meta = trigger?.meta;
|
|
8528
8564
|
return meta && typeof meta.repository === "string" && typeof meta.number === "number" && Number.isInteger(meta.number) && meta.isPr === false ? {
|
|
@@ -8531,8 +8567,8 @@ function issueOf(trigger) {
|
|
|
8531
8567
|
} : void 0;
|
|
8532
8568
|
}
|
|
8533
8569
|
/**
|
|
8534
|
-
* A pull request body that closes `issue` on merge
|
|
8535
|
-
* has a closing keyword (close/
|
|
8570
|
+
* A pull request body that closes `issue` on merge. Unchanged when the issue is in another repository, or when the
|
|
8571
|
+
* body already has a closing keyword (close/fix/resolve and their forms, any case, optional colon) before
|
|
8536
8572
|
* `#n`, `owner/repo#n` or the issue URL. Otherwise `Closes #n` is appended once, after a blank line.
|
|
8537
8573
|
*/
|
|
8538
8574
|
function linkIssue(body, repository, issue) {
|
|
@@ -8717,10 +8753,8 @@ function deliveryTools(host, ticketId) {
|
|
|
8717
8753
|
}
|
|
8718
8754
|
// src/app/tools/executor.ts
|
|
8719
8755
|
/**
|
|
8720
|
-
*
|
|
8721
|
-
*
|
|
8722
|
-
* `delivery.ts`; `pr_number` is removed (publish binds the PR thread) and `sendReplies` is removed; a GitHub thread must name a configured repository; a followed
|
|
8723
|
-
* thread is announced to the Router session through `notify` (M14); the report relay is the Runtime's (M14).
|
|
8756
|
+
* A GitHub thread must name a configured repository.
|
|
8757
|
+
* A followed thread is announced to the Router session through `notify`.
|
|
8724
8758
|
*/
|
|
8725
8759
|
function executorTools(host, ticket) {
|
|
8726
8760
|
const ticketId = ticket.id;
|
|
@@ -8999,7 +9033,6 @@ var RouterHost = class {
|
|
|
8999
9033
|
}
|
|
9000
9034
|
};
|
|
9001
9035
|
// src/sources/admission-context.ts
|
|
9002
|
-
/** Selects the first five thread messages plus five messages on either side of `sourceId`, recording omitted gap counts. */
|
|
9003
9036
|
function threadWindow(messages, sourceId) {
|
|
9004
9037
|
const trigger = messages.findIndex((message) => message.id === sourceId);
|
|
9005
9038
|
const selected = messages.map((message, index) => ({
|
|
@@ -9023,8 +9056,8 @@ function threadWindow(messages, sourceId) {
|
|
|
9023
9056
|
};
|
|
9024
9057
|
}
|
|
9025
9058
|
/**
|
|
9026
|
-
*
|
|
9027
|
-
*
|
|
9059
|
+
* Counts describe only the loaded snapshot, never unread history;
|
|
9060
|
+
* `status` reports fallback modes such as `chat_neighbours`.
|
|
9028
9061
|
*/
|
|
9029
9062
|
async function admissionContext(context, maxPages) {
|
|
9030
9063
|
const messages = [];
|
|
@@ -9055,13 +9088,12 @@ async function admissionContext(context, maxPages) {
|
|
|
9055
9088
|
}
|
|
9056
9089
|
// src/app/assessment.ts
|
|
9057
9090
|
/**
|
|
9058
|
-
* Proactive candidate assessment for the Router session: relays candidate cards, serves bounded context reads, and
|
|
9059
|
-
*
|
|
9060
|
-
* repository, image and file reads are
|
|
9061
|
-
* loaded through the kind's TriggerContext with no command runner
|
|
9062
|
-
* external data
|
|
9091
|
+
* Proactive candidate assessment for the Router session: relays candidate cards, serves bounded context reads, and
|
|
9092
|
+
* records outcomes. The Router session is the only assessor; the deterministic one stays in the Runtime. The
|
|
9093
|
+
* repository, image and file reads are not offered; GitHub originals are read through orchestrator_read. The thread
|
|
9094
|
+
* window is loaded through the kind's TriggerContext with no command runner. Every text a read or the context returns
|
|
9095
|
+
* is wrapped as external data. The checked-IM-text step is not performed.
|
|
9063
9096
|
*/
|
|
9064
|
-
/** Schema for a short lowercase kebab-case English ticket worktree name (1–4 words, at most 32 characters). */
|
|
9065
9097
|
const WorktreeNameSchema = z.string().max(32).regex(/^[a-z]+(?:-[a-z0-9]+){0,3}$/, "Use 1–4 short English words in lowercase kebab-case, at most 32 characters").describe("Meaningful short English ticket name, e.g. fix-plan-color. 1–4 lowercase words in kebab-case, at most 32 characters.");
|
|
9066
9098
|
/** The fields of the Router's assessment decision (`orchestrator_eval_decide`), before the cross-field rule. */
|
|
9067
9099
|
const AssessmentFields = z.object({
|
|
@@ -9079,23 +9111,18 @@ const AssessmentFields = z.object({
|
|
|
9079
9111
|
acknowledgement: AcknowledgementSchema.nullish().describe("For acceptance: one sentence in the source language, posted as a reply to the trigger; omit only when a matched rule asks for silence."),
|
|
9080
9112
|
reason: z.string().min(1).max(500)
|
|
9081
9113
|
}).strict();
|
|
9082
|
-
/** Schema for the Router session's proactive candidate assessment decision (`orchestrator_eval_decide`). */
|
|
9083
9114
|
const AssessmentSchema = AssessmentFields.refine((value) => value.decision !== "accept" || !directionAllowed(value.direction) || !value.rule_assessments.some((rule) => rule.matched) || !!value.worktree_name, {
|
|
9084
9115
|
message: "Accepted work requires a short English worktree_name",
|
|
9085
9116
|
path: ["worktree_name"]
|
|
9086
9117
|
});
|
|
9087
|
-
/** Character limits in the initial thread window: for the trigger itself, its two nearest neighbours, other window messages, and stub previews. */
|
|
9088
9118
|
const TRIGGER_CHARS = 4e3;
|
|
9089
9119
|
const NEAR_CHARS = 1200;
|
|
9090
9120
|
const WINDOW_CHARS = 400;
|
|
9091
9121
|
const STUB_CHARS = 50;
|
|
9092
|
-
/** Pages of the thread loaded for the initial window. */
|
|
9093
9122
|
const CONTEXT_PAGES = 5;
|
|
9094
9123
|
const FINISHED = "This assessment has finished: it was decided, timed out or restarted";
|
|
9095
9124
|
const EXPLICIT = "This is an explicit request, which is routed instead of assessed: use orchestrator_route with its trigger_id and kind accept, attach, stop or ignore";
|
|
9096
|
-
/** Message returned when assessing a Trigger whose Source provides no thread context. */
|
|
9097
9125
|
const NO_CONTEXT = "This Source gives no Thread context; assess the Trigger's own text and the batch.";
|
|
9098
|
-
/** Fields of `orchestrator_eval_read`: a message, the root, the parent, or a page of the thread. */
|
|
9099
9126
|
const EvalReadFields = z.object({
|
|
9100
9127
|
trigger_id: z.string().min(1).describe("Original candidate ID returned by orchestrator_eval; every reference is scoped to this trigger."),
|
|
9101
9128
|
resource: z.enum([
|
|
@@ -9108,7 +9135,6 @@ const EvalReadFields = z.object({
|
|
|
9108
9135
|
cursor: z.string().optional().describe("Only for thread: omit on the first page, then the next_cursor of the window or the cursor of next_read. Never a thread or message ID."),
|
|
9109
9136
|
offset: z.number().int().min(0).default(0).describe("Text pagination: copy next_offset; keep the other arguments unchanged.")
|
|
9110
9137
|
}).strict();
|
|
9111
|
-
/** Schema for bounded context requests during an active evaluation (`orchestrator_eval_read`). */
|
|
9112
9138
|
const EvalReadSchema = EvalReadFields.superRefine((read, ctx) => {
|
|
9113
9139
|
if (read.message_id !== void 0 && read.resource !== "message") ctx.addIssue({
|
|
9114
9140
|
code: "custom",
|
|
@@ -9119,21 +9145,15 @@ const EvalReadSchema = EvalReadFields.superRefine((read, ctx) => {
|
|
|
9119
9145
|
message: "cursor is only for thread. First page: {resource:\"thread\"}."
|
|
9120
9146
|
});
|
|
9121
9147
|
});
|
|
9122
|
-
/**
|
|
9148
|
+
/** Wraps the text as external data; `chars` reports the full length when it is cut. */
|
|
9123
9149
|
function cut$1(text, limit, source) {
|
|
9124
9150
|
return text.length <= limit ? { text: external(source, text) } : {
|
|
9125
9151
|
text: external(source, `${text.slice(0, limit)}…`),
|
|
9126
9152
|
chars: text.length
|
|
9127
9153
|
};
|
|
9128
9154
|
}
|
|
9129
|
-
/**
|
|
9130
|
-
* Implements the `Engine` `Assessor` port for proactive candidate evaluation in the Router session.
|
|
9131
|
-
* Relays each evaluation's candidate card to the Router session through the durable `assessment` effect, serves bounded thread
|
|
9132
|
-
* context reads while the evaluation runs, and converts the Router's assessment into an `AssessmentOutcome` for `Engine.decide`.
|
|
9133
|
-
* The Router's `orchestrator_eval*` tools call `context`, `read`, and `decide`.
|
|
9134
|
-
*/
|
|
9155
|
+
/** The Router's `orchestrator_eval*` tools call `context`, `read`, and `decide`. */
|
|
9135
9156
|
var MasterAssessor = class {
|
|
9136
|
-
/** Per-evaluation read caches and thread contexts, keyed by `evalId`. */
|
|
9137
9157
|
evals = /* @__PURE__ */ new Map();
|
|
9138
9158
|
host;
|
|
9139
9159
|
/** Registers the handler that relays each Eval's candidate card, which a busy Router session may delay. */
|
|
@@ -9149,11 +9169,9 @@ var MasterAssessor = class {
|
|
|
9149
9169
|
this.host.engine().candidateDelivered(evalId);
|
|
9150
9170
|
}, { limit: () => host.paused() ? 0 : 4 });
|
|
9151
9171
|
}
|
|
9152
|
-
/** Enqueues an `assessment` effect to deliver `candidate`'s card to the Router session. */
|
|
9153
9172
|
request(candidate) {
|
|
9154
9173
|
this.host.effects.enqueue("assessment", candidate.evalId, { triggerId: candidate.triggers[0].id });
|
|
9155
9174
|
}
|
|
9156
|
-
/** Whether `evalId` is still the active evaluation for `trigger`. */
|
|
9157
9175
|
live(evalId, trigger) {
|
|
9158
9176
|
return this.host.engine().candidate(trigger.id)?.evalId === evalId;
|
|
9159
9177
|
}
|
|
@@ -9162,7 +9180,10 @@ var MasterAssessor = class {
|
|
|
9162
9180
|
this.evals.delete(evalId);
|
|
9163
9181
|
this.host.effects.cancel("assessment", evalId);
|
|
9164
9182
|
}
|
|
9165
|
-
/**
|
|
9183
|
+
/**
|
|
9184
|
+
* Resolve the active evaluation for a trigger. Historical triggers have none, and explicit requests
|
|
9185
|
+
* are routed instead.
|
|
9186
|
+
*/
|
|
9166
9187
|
evalFor(triggerId) {
|
|
9167
9188
|
const candidate = this.host.engine().candidate(triggerId);
|
|
9168
9189
|
if (!candidate) throw new Error("No assessment is in progress for this trigger; it was already decided, timed out, or never reached assessment");
|
|
@@ -9172,7 +9193,7 @@ var MasterAssessor = class {
|
|
|
9172
9193
|
trigger: candidate.triggers.find((item) => item.id === triggerId)
|
|
9173
9194
|
};
|
|
9174
9195
|
}
|
|
9175
|
-
/**
|
|
9196
|
+
/** @throws when `evalId` has finished. */
|
|
9176
9197
|
reads(evalId, trigger) {
|
|
9177
9198
|
let reads = this.evals.get(evalId);
|
|
9178
9199
|
if (!reads) {
|
|
@@ -9187,7 +9208,6 @@ var MasterAssessor = class {
|
|
|
9187
9208
|
}
|
|
9188
9209
|
return reads;
|
|
9189
9210
|
}
|
|
9190
|
-
/** Loads and caches the initial bounded thread window once per evaluation. */
|
|
9191
9211
|
async seed(evalId, trigger, caller) {
|
|
9192
9212
|
caller?.throwIfAborted();
|
|
9193
9213
|
const reads = this.reads(evalId, trigger);
|
|
@@ -9220,11 +9240,11 @@ var MasterAssessor = class {
|
|
|
9220
9240
|
if (!this.live(evalId, trigger)) throw new Error(FINISHED);
|
|
9221
9241
|
}
|
|
9222
9242
|
/**
|
|
9223
|
-
* Returns the `orchestrator_eval` payload: the trigger's text once (up to `TRIGGER_CHARS`), batch peers, related
|
|
9224
|
-
* the thread, the Policy version its Eval was built from, and the initial thread window (the first five
|
|
9225
|
-
* either side of the trigger, cut to `WINDOW_CHARS` or `NEAR_CHARS` for its two nearest
|
|
9226
|
-
* as `STUB_CHARS` previews; `orchestrator_eval_read` reads any message in full
|
|
9227
|
-
* Assessment instructions live in the Router's prompt, not in this payload.
|
|
9243
|
+
* Returns the `orchestrator_eval` payload: the trigger's text once (up to `TRIGGER_CHARS`), batch peers, related
|
|
9244
|
+
* tickets on the thread, the Policy version its Eval was built from, and the initial thread window (the first five
|
|
9245
|
+
* messages and five on either side of the trigger, cut to `WINDOW_CHARS` or `NEAR_CHARS` for its two nearest
|
|
9246
|
+
* neighbours, with other loaded messages as `STUB_CHARS` previews; `orchestrator_eval_read` reads any message in full
|
|
9247
|
+
* by ID). Assessment instructions live in the Router's prompt, not in this payload.
|
|
9228
9248
|
*/
|
|
9229
9249
|
async context(triggerId, signal) {
|
|
9230
9250
|
const { candidate, trigger } = this.evalFor(triggerId);
|
|
@@ -9299,7 +9319,6 @@ var MasterAssessor = class {
|
|
|
9299
9319
|
thread
|
|
9300
9320
|
};
|
|
9301
9321
|
}
|
|
9302
|
-
/** Serves a bounded, paginated read of the evaluation's message, root, parent, or thread page for `orchestrator_eval_read`. */
|
|
9303
9322
|
async read(read, signal) {
|
|
9304
9323
|
const { candidate, trigger } = this.evalFor(read.trigger_id);
|
|
9305
9324
|
await this.seed(candidate.evalId, trigger, signal);
|
|
@@ -9349,11 +9368,10 @@ var MasterAssessor = class {
|
|
|
9349
9368
|
snapshots.set(cacheKey, body);
|
|
9350
9369
|
return textPage(read.resource, body, read.offset, 4e3);
|
|
9351
9370
|
}
|
|
9352
|
-
/** Converts the assessment into an outcome under current settings and passes it to `Engine.decide` for policy verification. */
|
|
9353
9371
|
async decide(triggerId, result, signal) {
|
|
9354
9372
|
const { candidate } = this.evalFor(triggerId);
|
|
9355
9373
|
signal?.throwIfAborted();
|
|
9356
|
-
const outcome = assessmentOutcome(result, this.host.settings()
|
|
9374
|
+
const outcome = assessmentOutcome(result, this.host.settings());
|
|
9357
9375
|
const decision = this.host.engine().decide(triggerId, {
|
|
9358
9376
|
...outcome,
|
|
9359
9377
|
evalId: candidate.evalId
|
|
@@ -9364,20 +9382,16 @@ var MasterAssessor = class {
|
|
|
9364
9382
|
reason: reasonText(decision.reason)
|
|
9365
9383
|
};
|
|
9366
9384
|
}
|
|
9367
|
-
/** Drops every evaluation's context and reads when the runtime stops. */
|
|
9368
9385
|
stop() {
|
|
9369
9386
|
this.evals.clear();
|
|
9370
9387
|
}
|
|
9371
9388
|
};
|
|
9372
9389
|
// src/app/tools/main.ts
|
|
9373
9390
|
/**
|
|
9374
|
-
*
|
|
9375
|
-
*
|
|
9376
|
-
* off ignores), so no tool bypasses it (D3); the eval reads are `message|root|parent|thread` (D4); the settings, policy-sync and
|
|
9377
|
-
* prompt tools are removed (ADR-9); the IM word limit is `HUMAN_WORDS`; an unexpected error is a refusal to retry, not a log line.
|
|
9391
|
+
* `orchestrator_route` `accept` honors the host accept mode (observe records what it would accept, off ignores),
|
|
9392
|
+
* so no tool bypasses it. An unexpected error is a refusal to retry.
|
|
9378
9393
|
*/
|
|
9379
9394
|
const RoutingNoteSchema = z.string().trim().refine((text) => wordCount(text) <= 30, `Routing note must not exceed 30 words (Chinese uses word segmentation). Shorten it; do not split it across deliveries.`).describe(`Optional routing background or explicit human constraint, at most 30 words. Never add implementation instructions or replace the original source.`);
|
|
9380
|
-
/** Schedules an Engine pass to execute the recorded routing decision and formats the tool response. */
|
|
9381
9395
|
function routed(host, result) {
|
|
9382
9396
|
host.requestPass();
|
|
9383
9397
|
const ticket = result.ticketId ? host.store.tickets.get(result.ticketId) : void 0;
|
|
@@ -9392,15 +9406,14 @@ const fromDecision = (decision) => ({
|
|
|
9392
9406
|
reason: reasonText(decision.reason),
|
|
9393
9407
|
ticketId: decision.ticketId
|
|
9394
9408
|
});
|
|
9395
|
-
/** A ticket the tool names must exist and still be open. */
|
|
9396
9409
|
function openTicket(host, id) {
|
|
9397
9410
|
const ticket = host.store.tickets.get(id);
|
|
9398
9411
|
if (!ticket) throw new UserError("unknownTicket");
|
|
9399
9412
|
if (ticket.ended) throw new UserError("ticketEnded");
|
|
9400
9413
|
}
|
|
9401
9414
|
/**
|
|
9402
|
-
* One routing decision
|
|
9403
|
-
*
|
|
9415
|
+
* One routing decision. The schema holds only its shape; each kind's fields are checked as its item runs (`FIELDS`),
|
|
9416
|
+
* so an item that lacks one, or whose value breaks a limit, is refused alone.
|
|
9404
9417
|
*/
|
|
9405
9418
|
const RouteItemSchema = z.object({
|
|
9406
9419
|
kind: z.enum([
|
|
@@ -9417,7 +9430,6 @@ const RouteItemSchema = z.object({
|
|
|
9417
9430
|
note: z.string().nullish().describe(`accept and attach: ${RoutingNoteSchema.description ?? ""}`),
|
|
9418
9431
|
reason: z.string().nullish().describe("ignore and stop: why, at most 500 characters; an ignore needs it. An ignore of an explicit request may be posted to its sender, so start with one plain sentence about the request, without internal terms or ids.")
|
|
9419
9432
|
}).strict();
|
|
9420
|
-
/** Each field's check, the kinds whose items read it and those that need it; an item of another kind leaves it unread. */
|
|
9421
9433
|
const FIELDS = {
|
|
9422
9434
|
ticket_id: {
|
|
9423
9435
|
check: z.string(),
|
|
@@ -9450,7 +9462,6 @@ const FIELDS = {
|
|
|
9450
9462
|
needs: ["ignore"]
|
|
9451
9463
|
}
|
|
9452
9464
|
};
|
|
9453
|
-
/** Why an item cannot run: a field its kind needs is missing, or one it reads breaks its limit. */
|
|
9454
9465
|
function invalid(item) {
|
|
9455
9466
|
for (const [field, { check, reads, needs }] of Object.entries(FIELDS)) {
|
|
9456
9467
|
const value = item[field];
|
|
@@ -9464,8 +9475,8 @@ function invalid(item) {
|
|
|
9464
9475
|
}
|
|
9465
9476
|
}
|
|
9466
9477
|
/**
|
|
9467
|
-
*
|
|
9468
|
-
* call's earlier items decided, with their batches.
|
|
9478
|
+
* Carries out one routing decision as its Engine command, in a transaction of its own.
|
|
9479
|
+
* `decided` holds the Triggers that this call's earlier items decided, with their batches.
|
|
9469
9480
|
*/
|
|
9470
9481
|
function route(host, item, decided) {
|
|
9471
9482
|
const note = item.note ? { note: item.note } : {};
|
|
@@ -9546,8 +9557,8 @@ function route(host, item, decided) {
|
|
|
9546
9557
|
}
|
|
9547
9558
|
}
|
|
9548
9559
|
/**
|
|
9549
|
-
* `orchestrator_route`: routing decisions for several triggers in one call, in order.
|
|
9550
|
-
* that is refused
|
|
9560
|
+
* `orchestrator_route`: routing decisions for several triggers in one call, in order.
|
|
9561
|
+
* Each runs as its own Engine command. One that is refused or fails is answered so; the others still run.
|
|
9551
9562
|
*/
|
|
9552
9563
|
function routeTool(host) {
|
|
9553
9564
|
return tool$1({
|
|
@@ -9720,8 +9731,7 @@ function mainTools(host) {
|
|
|
9720
9731
|
// src/app/cache.ts
|
|
9721
9732
|
/**
|
|
9722
9733
|
* In-memory LRU cache bounded by total UTF-16 code units, always retaining the most recently inserted entry.
|
|
9723
|
-
* Runtime uses separate instances
|
|
9724
|
-
* Changes from the reference: the constructor parameter is a declared field, assigned in the constructor body.
|
|
9734
|
+
* Runtime uses separate instances for paginated resource snapshots (`orchestrator_read`) and resolved message links.
|
|
9725
9735
|
*/
|
|
9726
9736
|
var TextCache = class {
|
|
9727
9737
|
texts = /* @__PURE__ */ new Map();
|
|
@@ -9772,7 +9782,10 @@ function jsonLines(write, env) {
|
|
|
9772
9782
|
write(mask(JSON.stringify(record)) + "\n");
|
|
9773
9783
|
};
|
|
9774
9784
|
}
|
|
9775
|
-
/**
|
|
9785
|
+
/**
|
|
9786
|
+
* Executor tools; in deterministic Router mode a ready report also queues its summary as the result, in the same
|
|
9787
|
+
* transaction that records the report (so no reader or crash sees the ticket ended without its result).
|
|
9788
|
+
*/
|
|
9776
9789
|
function workerTools(host, ticket, atomic) {
|
|
9777
9790
|
const engine = () => {
|
|
9778
9791
|
const inner = host.engine();
|
|
@@ -9842,7 +9855,7 @@ var GateAssessor = class {
|
|
|
9842
9855
|
const candidate = this.host.engine().candidate(triggerId);
|
|
9843
9856
|
if (candidate?.evalId !== evalId) return;
|
|
9844
9857
|
const settings = this.host.settings();
|
|
9845
|
-
const outcome = assessmentOutcome(deterministicAssessment(candidate, settings), settings
|
|
9858
|
+
const outcome = assessmentOutcome(deterministicAssessment(candidate, settings), settings);
|
|
9846
9859
|
try {
|
|
9847
9860
|
this.host.engine().decide(triggerId, {
|
|
9848
9861
|
...outcome,
|
|
@@ -10339,13 +10352,22 @@ var Runtime = class {
|
|
|
10339
10352
|
});
|
|
10340
10353
|
this.passes.request();
|
|
10341
10354
|
}
|
|
10342
|
-
/**
|
|
10355
|
+
/**
|
|
10356
|
+
* Re-reads the config file and applies it: the policy part from the next Trigger on (a changed policy version is an
|
|
10357
|
+
* input to open tickets), startup-only keys are kept and reported. A refused file changes nothing. `network.allow`
|
|
10358
|
+
* and `models.worker` apply to worker sessions opened after the reload; `retention.endedTicketDays` applies from the
|
|
10359
|
+
* next retention evaluation. Calls are serialised.
|
|
10360
|
+
*/
|
|
10343
10361
|
reload() {
|
|
10344
10362
|
const next = this.reloads.then(() => this.reloadOnce());
|
|
10345
10363
|
this.reloads = next.catch(() => void 0);
|
|
10346
10364
|
return next;
|
|
10347
10365
|
}
|
|
10348
|
-
/**
|
|
10366
|
+
/**
|
|
10367
|
+
* Reassembles the worker prompt; when its team sections or rules changed, queues a notice with the changed sections
|
|
10368
|
+
* for every open worker session (same key per ticket: only the latest is delivered). Returns the number queued.
|
|
10369
|
+
* Called on reload.
|
|
10370
|
+
*/
|
|
10349
10371
|
refreshPrompts() {
|
|
10350
10372
|
const routerChange = this.router ? this.prompts.refresh("router") : void 0;
|
|
10351
10373
|
if (routerChange?.notice) {
|
|
@@ -10648,7 +10670,10 @@ function healthCommand(argv, io, now = Date.now) {
|
|
|
10648
10670
|
return 0;
|
|
10649
10671
|
}
|
|
10650
10672
|
// src/cli/prompts.ts
|
|
10651
|
-
/**
|
|
10673
|
+
/**
|
|
10674
|
+
* `superagent-claude prompts diff`: reports built-in default changes of the team sections from the database opened
|
|
10675
|
+
* read-only; it takes no lock and needs no running daemon.
|
|
10676
|
+
*/
|
|
10652
10677
|
function promptsCommand(argv, io) {
|
|
10653
10678
|
if (argv[0] !== "diff") {
|
|
10654
10679
|
io.err(`superagent-claude: unknown prompts command '${argv[0] ?? ""}'\nUsage: superagent-claude prompts diff [--config <path>]
|
|
@@ -10697,9 +10722,6 @@ function promptsCommand(argv, io) {
|
|
|
10697
10722
|
return 0;
|
|
10698
10723
|
}
|
|
10699
10724
|
// src/app/status.ts
|
|
10700
|
-
/**
|
|
10701
|
-
* Read models of the status and needs CLI over the read-only database: running work, idle sessions and waiting work, recent errors, repository access, consent, and the tickets that need a person; and the masking of secret values in their output.
|
|
10702
|
-
*/
|
|
10703
10725
|
/** KV family the repository Source writes per repository: { accessible, push, at, error? }. */
|
|
10704
10726
|
const REPOSITORY_ACCESS_PREFIX = "repo-access:";
|
|
10705
10727
|
function rows(reader, sql, ...params) {
|
|
@@ -10714,7 +10736,10 @@ function thread(reader, key) {
|
|
|
10714
10736
|
const row = reader.db.prepare("SELECT value FROM thread WHERE key = ?").get(key);
|
|
10715
10737
|
return row ? JSON.parse(String(row.value)) : void 0;
|
|
10716
10738
|
}
|
|
10717
|
-
/**
|
|
10739
|
+
/**
|
|
10740
|
+
* When a running Assignment's Session went idle: its last turn ended (idleAt), nothing waits to be delivered,
|
|
10741
|
+
* and no message delivered to it is unread or was read after that.
|
|
10742
|
+
*/
|
|
10718
10743
|
function idleSince(ticket, assignment) {
|
|
10719
10744
|
const at = assignment.idleAt;
|
|
10720
10745
|
if (!at || ticket.undelivered.length > 0) return void 0;
|
|
@@ -10977,7 +11002,10 @@ const FAKE_SCRIPT = ({ index }) => index === 0 ? { tools: [{
|
|
|
10977
11002
|
summary: "Done: the fake worker finished"
|
|
10978
11003
|
}
|
|
10979
11004
|
}] } : {};
|
|
10980
|
-
/**
|
|
11005
|
+
/**
|
|
11006
|
+
* With SUPERAGENT_CLAUDE_FAKE_HOLD=1 (read only with --fake) the fake worker never ends a turn, so a test can stop
|
|
11007
|
+
* the daemon while an Assignment runs.
|
|
11008
|
+
*/
|
|
10981
11009
|
const HOLD_SCRIPT = () => ({ hold: new Promise(() => {}) });
|
|
10982
11010
|
const fakeScript = (env) => env.SUPERAGENT_CLAUDE_FAKE_HOLD === "1" ? HOLD_SCRIPT : FAKE_SCRIPT;
|
|
10983
11011
|
const HELP = [
|