@coreplane/switchboard 1.233.0 → 1.234.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/deploy/cloudflare-resident/worker.ts +200 -214
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +13 -7
- package/dist/assets/src/core/provider.ts +9 -6
- package/dist/assets/src/core/runEvents.ts +18 -0
- package/dist/assets/src/execution/residentCleanliness.ts +45 -5
- package/dist/assets/src/execution/residentRebind.ts +92 -77
- package/dist/assets/src/execution/residentReuse.ts +7 -18
- package/dist/assets/src/execution/residentSteps.ts +0 -1
- package/dist/assets/web/dist/.vite/manifest.json +67 -28
- package/dist/assets/web/dist/assets/{ResidentDetailPage-V4K35SYX.js → ResidentDetailPage-C_WNBUao.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-3HuYaDwD.js → ResidentsIndexPage-D1sTyaZ_.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-C_bcTSjA.js +5 -0
- package/dist/assets/web/dist/assets/RunRoutePage-BWTi_6PG.js +10 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-H6GkSv0a.js +1 -0
- package/dist/assets/web/dist/assets/{ScheduledPage-dfrjQ5E9.js → ScheduledPage-BLcPzm1S.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-DexKtvUC.js → StatusDot-Ct6AtHhS.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DGQ2gu9M.js → Tooltip-qol5Boof.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CIhTEwN7.js +1 -0
- package/dist/assets/web/dist/assets/{dist-boTdDLF4.js → dist-DwtJj-ou.js} +1 -1
- package/dist/assets/web/dist/assets/favicon-BQsePYv5.js +1 -0
- package/dist/assets/web/dist/assets/indexRow-Bnj883ii.js +1 -0
- package/dist/assets/web/dist/assets/{main-BHhLVMQh.js → main-CRtlGxRm.js} +2 -2
- package/dist/assets/web/dist/assets/main-Dm11o0hc.css +1 -0
- package/dist/cli.js +1156 -570
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/RunRoutePage-Dr867-Ha.js +0 -13
- package/dist/assets/web/dist/assets/RunsIndexPage-D_lpXsr8.js +0 -1
- package/dist/assets/web/dist/assets/favicon-CWPcvWvp.js +0 -1
- package/dist/assets/web/dist/assets/main-CAVqMbiX.css +0 -1
|
@@ -87,6 +87,7 @@ import { parseReadonly, planReadonlyAttach } from "../../src/execution/residentR
|
|
|
87
87
|
import { decideWorktree, parseReuse, type WorktreeFacts } from "../../src/execution/residentReuse.js";
|
|
88
88
|
import {
|
|
89
89
|
boundByFor,
|
|
90
|
+
canReturnToDefault,
|
|
90
91
|
parseOwnPr,
|
|
91
92
|
parsePushed,
|
|
92
93
|
parseRefByDefault,
|
|
@@ -94,10 +95,12 @@ import {
|
|
|
94
95
|
rebindRefused,
|
|
95
96
|
rebindVerdict,
|
|
96
97
|
rememberOwnBranches,
|
|
98
|
+
returnToDefault,
|
|
97
99
|
type BoundBy,
|
|
98
100
|
type OwnBranch,
|
|
99
101
|
type OwnPr,
|
|
100
102
|
type PushedBranch,
|
|
103
|
+
type Returned,
|
|
101
104
|
type Rebound,
|
|
102
105
|
type RebindRefused,
|
|
103
106
|
type RebindTreeFacts,
|
|
@@ -112,7 +115,12 @@ import {
|
|
|
112
115
|
type DepCacheMaterialization,
|
|
113
116
|
type ThreadDepsMechanism,
|
|
114
117
|
} from "../../src/execution/residentDepCache.js";
|
|
115
|
-
import {
|
|
118
|
+
import {
|
|
119
|
+
leftBehindOf,
|
|
120
|
+
parseWorktreeCleanliness,
|
|
121
|
+
worktreeCleanlinessScript,
|
|
122
|
+
type LeftBehind,
|
|
123
|
+
} from "../../src/execution/residentCleanliness.js";
|
|
116
124
|
import {
|
|
117
125
|
base64LengthOf,
|
|
118
126
|
chunkPlan,
|
|
@@ -994,15 +1002,17 @@ interface ThreadBinding {
|
|
|
994
1002
|
* binding made before the field: read as `default` iff `ref` is the default
|
|
995
1003
|
* branch. Only a `default` binding may ever move. */
|
|
996
1004
|
boundBy?: BoundBy;
|
|
997
|
-
/** The
|
|
998
|
-
*
|
|
999
|
-
*
|
|
1005
|
+
/** The move a binding may make (item 16): from the default it was bound to,
|
|
1006
|
+
* onto the branch the thread's own run opened a pull request on. Set, the
|
|
1007
|
+
* binding moves again only after the move is returned (`returnedAt`: the
|
|
1008
|
+
* branch was gone from the mirror and the binding went back to the
|
|
1009
|
+
* default), and then for the next own pull request. */
|
|
1000
1010
|
rebound?: Rebound;
|
|
1001
1011
|
/** The branches the thread's own runs pushed and the pull requests they
|
|
1002
1012
|
* head, as each run's release handed them over (`/detach` `pushed`, item
|
|
1003
1013
|
* 16a) — written before any eviction decision, so the fact outlives the
|
|
1004
|
-
* tree:
|
|
1005
|
-
* lets the follow-up's attach move onto the branch and
|
|
1014
|
+
* tree: the tree is released the moment its run ends, and this is what
|
|
1015
|
+
* lets the follow-up's attach move onto the branch and provision the tree
|
|
1006
1016
|
* there. Newest last; the oldest fall off past `OWN_BRANCHES_MAX`. */
|
|
1007
1017
|
ownBranches?: OwnBranch[];
|
|
1008
1018
|
/** Allocated OS user (worker2..worker17); "" once evicted (pool released). */
|
|
@@ -1088,6 +1098,20 @@ interface AttachOk {
|
|
|
1088
1098
|
/** The caller asked for that move and the binding stood: the branch, the PR
|
|
1089
1099
|
* and why — so the bot can say where the follow-up runs and why. */
|
|
1090
1100
|
rebindRefused?: RebindRefused;
|
|
1101
|
+
/** This attach moved the binding BACK to the default branch (item 16's
|
|
1102
|
+
* second movement): the branch a rebind had moved it onto is gone from the
|
|
1103
|
+
* mirror, so the tree was provisioned on the default instead of the attach
|
|
1104
|
+
* failing `unknown-ref`. From where, to where, which PR's branch it was. */
|
|
1105
|
+
returned?: Returned;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/** What `POST /detach` answers: whether the pool user went back, why not, and
|
|
1109
|
+
* — on a release — what the tree still held (item 16a), now gone with it. */
|
|
1110
|
+
interface DetachAnswer {
|
|
1111
|
+
released: boolean;
|
|
1112
|
+
reason?: string;
|
|
1113
|
+
user?: string;
|
|
1114
|
+
leftBehind?: LeftBehind;
|
|
1091
1115
|
}
|
|
1092
1116
|
|
|
1093
1117
|
/** Why the caller's `refHint` is what it is (item 16), beside the hint itself:
|
|
@@ -1105,17 +1129,11 @@ const NO_REF_HINT_REASON: RefHintReason = { ownPr: null, refByDefault: false };
|
|
|
1105
1129
|
|
|
1106
1130
|
/** What the rebind decided under the mutex (item 16): nothing to move (the
|
|
1107
1131
|
* row is already on the branch), a named refusal, or the move — written on
|
|
1108
|
-
* the row
|
|
1109
|
-
* recreate the tree at. */
|
|
1132
|
+
* the row; the attach that follows provisions the tree at the moved ref. */
|
|
1110
1133
|
type RebindOutcome =
|
|
1111
1134
|
| { kind: "none"; binding: ThreadBinding }
|
|
1112
1135
|
| { kind: "refuse"; refused: RebindRefused }
|
|
1113
|
-
| { kind: "rebound"; moved: ThreadBinding; rebound: Rebound
|
|
1114
|
-
/** The move was the record alone — a dirty tree already on the branch, no
|
|
1115
|
-
* checkout run — with the verdict's `note` for the log line. The attach
|
|
1116
|
-
* that follows must keep that tree (`keepTree`, the worktree step's word
|
|
1117
|
-
* for it): the promise the note makes is the attach's to keep. */
|
|
1118
|
-
| { kind: "rebound"; moved: ThreadBinding; rebound: Rebound; keepTree: true; note: string };
|
|
1136
|
+
| { kind: "rebound"; moved: ThreadBinding; rebound: Rebound };
|
|
1119
1137
|
|
|
1120
1138
|
/** Result of one /op test/build execution. `ok` is the command's
|
|
1121
1139
|
* verdict — a failing test run is a RESULT with ok:false, never an error. */
|
|
@@ -4514,10 +4532,12 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4514
4532
|
|
|
4515
4533
|
// The binding's ref wins for the thread's whole life, with one exception
|
|
4516
4534
|
// (item 16): a thread bound to the repo default for want of a named branch
|
|
4517
|
-
// moves, once
|
|
4518
|
-
// request on — when that branch is
|
|
4519
|
-
// the
|
|
4520
|
-
// the attach (fetch,
|
|
4535
|
+
// moves, once per pull request, onto the branch its own run opened a pull
|
|
4536
|
+
// request on — when that branch is the thread's own and the mirror holds
|
|
4537
|
+
// it. A decision about the binding alone, made here, before the ref is
|
|
4538
|
+
// chosen, so the rest of the attach (fetch, the worktree step that
|
|
4539
|
+
// provisions the tree clean at the moved ref, deps, credentials) runs on
|
|
4540
|
+
// the moved ref.
|
|
4521
4541
|
const rebind = await this.rebindToOwnPr(
|
|
4522
4542
|
stored.get(threadBindingKey(threadKey)) as ThreadBinding | undefined,
|
|
4523
4543
|
reason.ownPr,
|
|
@@ -4573,7 +4593,6 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4573
4593
|
refHint,
|
|
4574
4594
|
wantSha,
|
|
4575
4595
|
reuse,
|
|
4576
|
-
keepTree: rebind.keepTree === true,
|
|
4577
4596
|
slug,
|
|
4578
4597
|
t0,
|
|
4579
4598
|
facts,
|
|
@@ -4591,54 +4610,36 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4591
4610
|
|
|
4592
4611
|
/** Item 16's one exception to the sticky binding: a thread bound to the repo
|
|
4593
4612
|
* default for want of a named branch moves onto the branch its own run
|
|
4594
|
-
* opened a pull request on (`ownPr`), once
|
|
4595
|
-
* pure `rebindPlan` / `rebindVerdict` of
|
|
4596
|
-
*
|
|
4597
|
-
*
|
|
4598
|
-
*
|
|
4599
|
-
*
|
|
4600
|
-
*
|
|
4601
|
-
*
|
|
4602
|
-
*
|
|
4603
|
-
*
|
|
4604
|
-
*
|
|
4605
|
-
*
|
|
4606
|
-
* the whole attach's. The binding records the move (`rebound`); the answer
|
|
4607
|
-
* carries it, or the named refusal, so the bot can say where the follow-up
|
|
4608
|
-
* runs and why. A refusal never fails the attach. */
|
|
4613
|
+
* opened a pull request on (`ownPr`), once per pull request. A decision
|
|
4614
|
+
* about the BINDING alone — the pure `rebindPlan` / `rebindVerdict` of
|
|
4615
|
+
* src/execution/residentRebind.ts: the branch must be the thread's own,
|
|
4616
|
+
* remembered from a release (`ownBranches`) or, when nothing was
|
|
4617
|
+
* remembered, a local branch of the thread's surviving tree, measured as
|
|
4618
|
+
* the thread user — and the mirror must hold it (`moveOntoOwnBranch`), since
|
|
4619
|
+
* the attach that follows provisions the tree at the moved ref from the
|
|
4620
|
+
* mirror as it provisions any tree (item 17: clean at the ref's tip, a tree
|
|
4621
|
+
* left dirty or on the old branch recreated, the binding's path kept). Under
|
|
4622
|
+
* the mirror mutex, on the row as it stands there. The binding records the
|
|
4623
|
+
* move (`rebound`); the answer carries it, or the named refusal, so the bot
|
|
4624
|
+
* can say where the follow-up runs and why. A refusal never fails the attach. */
|
|
4609
4625
|
private async rebindToOwnPr(
|
|
4610
4626
|
prior: ThreadBinding | undefined,
|
|
4611
4627
|
ownPr: OwnPr | null,
|
|
4612
4628
|
reuse: boolean,
|
|
4613
4629
|
defaultRef: string,
|
|
4614
4630
|
slug: string,
|
|
4615
|
-
): Promise<
|
|
4616
|
-
| {
|
|
4617
|
-
binding: ThreadBinding | undefined;
|
|
4618
|
-
rebound?: Rebound;
|
|
4619
|
-
rebindRefused?: RebindRefused;
|
|
4620
|
-
/** The move was the record alone — the tree is dirty with the branch
|
|
4621
|
-
* already checked out — and the attach must keep that tree as it
|
|
4622
|
-
* stands: the worktree step's item 17 wipe of a dirty tree would
|
|
4623
|
-
* destroy the very work the rebind declined to touch. */
|
|
4624
|
-
keepTree?: true;
|
|
4625
|
-
}
|
|
4626
|
-
| ThreadErr
|
|
4627
|
-
> {
|
|
4631
|
+
): Promise<{ binding: ThreadBinding | undefined; rebound?: Rebound; rebindRefused?: RebindRefused } | ThreadErr> {
|
|
4628
4632
|
const plan = rebindPlan({ ownPr, reuse, binding: prior, defaultRef });
|
|
4629
4633
|
if (plan.kind === "none" || prior === undefined) return { binding: prior };
|
|
4630
4634
|
if (plan.kind === "refuse") return { binding: prior, rebindRefused: plan.refused };
|
|
4631
4635
|
const key = threadBindingKey(prior.threadKey);
|
|
4632
|
-
//
|
|
4633
|
-
//
|
|
4634
|
-
//
|
|
4635
|
-
//
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
(plan.kind === "recreate" || plan.own) && githubAppConfigured(this.env)
|
|
4640
|
-
? ((await mintRepoScopedToken(this.env, slug).catch(() => null))?.token ?? null)
|
|
4641
|
-
: null;
|
|
4636
|
+
// The tree is provisioned at the branch from the mirror, so the mirror
|
|
4637
|
+
// must hold it first: a branch pushed since the last refresh cycle is
|
|
4638
|
+
// fetched for — with a token minted here, before the mutex, like the
|
|
4639
|
+
// attach's own recovery fetch.
|
|
4640
|
+
const fetchToken = githubAppConfigured(this.env)
|
|
4641
|
+
? ((await mintRepoScopedToken(this.env, slug).catch(() => null))?.token ?? null)
|
|
4642
|
+
: null;
|
|
4642
4643
|
let outcome: RebindOutcome;
|
|
4643
4644
|
try {
|
|
4644
4645
|
outcome = (
|
|
@@ -4653,79 +4654,25 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4653
4654
|
const again = rebindPlan({ ownPr, reuse, binding: current, defaultRef });
|
|
4654
4655
|
if (again.kind === "none") return { kind: "none", binding: current };
|
|
4655
4656
|
if (again.kind === "refuse") return again;
|
|
4656
|
-
if (again.kind === "
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
const branch = await this.threadRun(
|
|
4665
|
-
current.user,
|
|
4666
|
-
wt,
|
|
4667
|
-
`git rev-parse --verify --quiet ${shellQuote(`refs/heads/${again.to}`)}`,
|
|
4668
|
-
DEFAULT_EXEC_TIMEOUT_MS,
|
|
4669
|
-
);
|
|
4670
|
-
tree.branchExists = branch.exitCode === 0;
|
|
4671
|
-
const status = await this.threadRun(
|
|
4672
|
-
current.user,
|
|
4673
|
-
wt,
|
|
4674
|
-
"git status --porcelain -uno",
|
|
4675
|
-
DEFAULT_EXEC_TIMEOUT_MS,
|
|
4676
|
-
);
|
|
4677
|
-
tree.readable = status.exitCode === 0;
|
|
4678
|
-
if (tree.readable) tree.dirty = status.stdout.trim() !== "";
|
|
4679
|
-
if (tree.dirty) {
|
|
4680
|
-
// A dirty tree is refused unless its HEAD is already the branch
|
|
4681
|
-
// (the run made it here and left an edit after pushing): the
|
|
4682
|
-
// one fact that tells the two apart, measured only when it
|
|
4683
|
-
// decides anything.
|
|
4684
|
-
const head = await this.threadRun(
|
|
4657
|
+
if (again.kind === "measure" && !again.own) {
|
|
4658
|
+
// Nothing remembered from a release: the branch must be a local
|
|
4659
|
+
// branch of the thread's surviving tree — the physical trace of
|
|
4660
|
+
// this thread's run having made it — measured as the thread user.
|
|
4661
|
+
const wt = current.worktreePath;
|
|
4662
|
+
const tree: RebindTreeFacts = { exists: (await this.run(["test", "-d", `${wt}/.git`])).exitCode === 0 };
|
|
4663
|
+
if (tree.exists) {
|
|
4664
|
+
const branch = await this.threadRun(
|
|
4685
4665
|
current.user,
|
|
4686
4666
|
wt,
|
|
4687
|
-
|
|
4667
|
+
`git rev-parse --verify --quiet ${shellQuote(`refs/heads/${again.to}`)}`,
|
|
4688
4668
|
DEFAULT_EXEC_TIMEOUT_MS,
|
|
4689
4669
|
);
|
|
4690
|
-
|
|
4691
|
-
}
|
|
4692
|
-
}
|
|
4693
|
-
// Judged on the re-read plan: it carries whether the branch is the
|
|
4694
|
-
// thread's own, which the pre-lock plan lacks when it was a recreate.
|
|
4695
|
-
const verdict = rebindVerdict(again, tree);
|
|
4696
|
-
if (verdict.kind === "recreate") return this.recreateAtOwnBranch(current, again, fetchToken);
|
|
4697
|
-
if (verdict.kind !== "rebind") return verdict;
|
|
4698
|
-
if (verdict.checkout) {
|
|
4699
|
-
const startedAt = systemClock();
|
|
4700
|
-
const checkout = await this.threadRun(
|
|
4701
|
-
current.user,
|
|
4702
|
-
wt,
|
|
4703
|
-
`git checkout --quiet ${shellQuote(again.to)}`,
|
|
4704
|
-
DEFAULT_EXEC_TIMEOUT_MS,
|
|
4705
|
-
);
|
|
4706
|
-
this.stepTrace.getStore()?.record("rebind-checkout", {
|
|
4707
|
-
startedAt,
|
|
4708
|
-
endedAt: systemClock(),
|
|
4709
|
-
exitCode: checkout.exitCode,
|
|
4710
|
-
timedOut: checkout.timedOut,
|
|
4711
|
-
});
|
|
4712
|
-
if (checkout.exitCode !== 0) {
|
|
4713
|
-
const detail =
|
|
4714
|
-
checkout.stderr.trim().split("\n")[0]?.slice(0, 200) || `git checkout exited ${checkout.exitCode}`;
|
|
4715
|
-
return { kind: "refuse", refused: rebindRefused(again, "checkout-failed", detail) };
|
|
4670
|
+
tree.branchExists = branch.exitCode === 0;
|
|
4716
4671
|
}
|
|
4672
|
+
const verdict = rebindVerdict(again, tree);
|
|
4673
|
+
if (verdict.kind === "refuse") return verdict;
|
|
4717
4674
|
}
|
|
4718
|
-
|
|
4719
|
-
from: current.ref,
|
|
4720
|
-
to: again.to,
|
|
4721
|
-
pr: again.pr,
|
|
4722
|
-
at: new Date(systemClock()).toISOString(),
|
|
4723
|
-
};
|
|
4724
|
-
const moved: ThreadBinding = { ...current, ref: again.to, rebound };
|
|
4725
|
-
await this.ctx.storage.put(key, moved);
|
|
4726
|
-
return verdict.checkout
|
|
4727
|
-
? { kind: "rebound", moved, rebound, keepTree: false }
|
|
4728
|
-
: { kind: "rebound", moved, rebound, keepTree: true, note: verdict.note };
|
|
4675
|
+
return this.moveOntoOwnBranch(current, again, fetchToken);
|
|
4729
4676
|
}, ATTACH_MUTEX_WAIT_MS)
|
|
4730
4677
|
).value;
|
|
4731
4678
|
} catch (err) {
|
|
@@ -4743,28 +4690,22 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4743
4690
|
return { binding: prior, rebindRefused: outcome.refused };
|
|
4744
4691
|
}
|
|
4745
4692
|
const { moved, rebound } = outcome;
|
|
4746
|
-
if (!outcome.keepTree) {
|
|
4747
|
-
console.log(
|
|
4748
|
-
`attach ${prior.threadKey}: rebound ${rebound.from} → ${rebound.to} (the thread's own pull request #${rebound.pr})`,
|
|
4749
|
-
);
|
|
4750
|
-
return { binding: moved, rebound };
|
|
4751
|
-
}
|
|
4752
4693
|
console.log(
|
|
4753
|
-
`attach ${prior.threadKey}: rebound ${rebound.from} → ${rebound.to} (the thread's own pull request #${rebound.pr})
|
|
4694
|
+
`attach ${prior.threadKey}: rebound ${rebound.from} → ${rebound.to} (the thread's own pull request #${rebound.pr}); the tree is provisioned at it`,
|
|
4754
4695
|
);
|
|
4755
|
-
return { binding: moved, rebound
|
|
4756
|
-
}
|
|
4757
|
-
|
|
4758
|
-
/** The move
|
|
4759
|
-
*
|
|
4760
|
-
*
|
|
4761
|
-
*
|
|
4762
|
-
*
|
|
4763
|
-
*
|
|
4764
|
-
*
|
|
4765
|
-
*
|
|
4766
|
-
*
|
|
4767
|
-
private async
|
|
4696
|
+
return { binding: moved, rebound };
|
|
4697
|
+
}
|
|
4698
|
+
|
|
4699
|
+
/** The move itself (item 16): the binding's ref becomes the branch, and the
|
|
4700
|
+
* attach that follows provisions the tree at it — `ensureThreadWorktree`
|
|
4701
|
+
* clones the branch from the mirror when the tree is gone, dirty or on the
|
|
4702
|
+
* old ref. So the mirror must hold the branch: one pushed since the last
|
|
4703
|
+
* refresh cycle is fetched for here, under the mutex; a branch still
|
|
4704
|
+
* missing after the fetch (deleted after a merge, or never pushed) is a
|
|
4705
|
+
* `branch-absent` refusal and the binding stands — the attach then goes on
|
|
4706
|
+
* at the ref it had, never `unknown-ref`. Nothing on disk is touched here:
|
|
4707
|
+
* the row is written, the tree is the attach's. */
|
|
4708
|
+
private async moveOntoOwnBranch(
|
|
4768
4709
|
current: ThreadBinding,
|
|
4769
4710
|
plan: { to: string; pr: number },
|
|
4770
4711
|
fetchToken: string | null,
|
|
@@ -4795,7 +4736,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4795
4736
|
refused: rebindRefused(
|
|
4796
4737
|
plan,
|
|
4797
4738
|
"branch-absent",
|
|
4798
|
-
`the
|
|
4739
|
+
`the mirror does not hold ${JSON.stringify(plan.to)} even after a fetch (deleted after a merge, or never pushed); the tree cannot be provisioned at it`,
|
|
4799
4740
|
),
|
|
4800
4741
|
};
|
|
4801
4742
|
}
|
|
@@ -4807,13 +4748,43 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4807
4748
|
};
|
|
4808
4749
|
const moved: ThreadBinding = { ...current, ref: plan.to, rebound };
|
|
4809
4750
|
await this.ctx.storage.put(threadBindingKey(current.threadKey), moved);
|
|
4810
|
-
return { kind: "rebound", moved, rebound
|
|
4751
|
+
return { kind: "rebound", moved, rebound };
|
|
4752
|
+
}
|
|
4753
|
+
|
|
4754
|
+
/** Item 16's second movement, decided where the fact is established — under
|
|
4755
|
+
* the mirror mutex, after the attach's fetch found the bound ref gone: a
|
|
4756
|
+
* binding a rebind moved onto its own pull request's branch, that branch
|
|
4757
|
+
* now deleted (its pull request merged), goes back to the default it was
|
|
4758
|
+
* bound to, so the run starts clean there (item 17) instead of the thread
|
|
4759
|
+
* failing `unknown-ref` for the rest of its life. The row as it stands is
|
|
4760
|
+
* re-read and re-judged (`canReturnToDefault`: a ref a person named never
|
|
4761
|
+
* returns — that branch is theirs to sort out), the move stamped returned
|
|
4762
|
+
* so the thread may follow its next pull request, and the answer carries
|
|
4763
|
+
* the move back for the card. A row that no longer qualifies — another
|
|
4764
|
+
* attach moved it meanwhile — is answered as it stands, nothing written. */
|
|
4765
|
+
private async returnBindingToDefault(
|
|
4766
|
+
binding: ThreadBinding,
|
|
4767
|
+
defaultRef: string,
|
|
4768
|
+
): Promise<{ binding: ThreadBinding; returned?: Returned }> {
|
|
4769
|
+
const key = threadBindingKey(binding.threadKey);
|
|
4770
|
+
const current = (await this.ctx.storage.get<ThreadBinding>(key)) ?? binding;
|
|
4771
|
+
if (current.rebound === undefined || !canReturnToDefault(current, defaultRef)) return { binding: current };
|
|
4772
|
+
const back = returnToDefault(
|
|
4773
|
+
{ ...current, rebound: current.rebound },
|
|
4774
|
+
defaultRef,
|
|
4775
|
+
new Date(systemClock()).toISOString(),
|
|
4776
|
+
);
|
|
4777
|
+
await this.ctx.storage.put(key, back.binding);
|
|
4778
|
+
console.log(
|
|
4779
|
+
`attach ${binding.threadKey}: ${back.returned.from} is gone from the mirror (the thread's own pull request #${back.returned.pr}) — returned to ${back.returned.to}; the tree is provisioned there`,
|
|
4780
|
+
);
|
|
4781
|
+
return back;
|
|
4811
4782
|
}
|
|
4812
4783
|
|
|
4813
4784
|
/** The release's word on what the run pushed (`/detach` `pushed`, item
|
|
4814
|
-
* 16a), remembered on the binding BEFORE the detach decides anything:
|
|
4815
|
-
*
|
|
4816
|
-
*
|
|
4785
|
+
* 16a), remembered on the binding BEFORE the detach decides anything: the
|
|
4786
|
+
* tree evicted, a busy one kept, an already-evicted binding answered as
|
|
4787
|
+
* such — and in every case the fact must outlive the
|
|
4817
4788
|
* tree, since it is what a follow-up's rebind onto the thread's own pull
|
|
4818
4789
|
* request branch reads once the tree is gone. No binding → nothing to
|
|
4819
4790
|
* remember on (the detach answers its 404 next). */
|
|
@@ -4835,10 +4806,6 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4835
4806
|
refHint: string | null;
|
|
4836
4807
|
wantSha: string | null;
|
|
4837
4808
|
reuse: boolean;
|
|
4838
|
-
/** The rebind moved the record alone over a dirty tree already on the
|
|
4839
|
-
* branch (item 16) and promised not to touch it: the worktree step keeps
|
|
4840
|
-
* the tree as it stands instead of item 17's dirty wipe. */
|
|
4841
|
-
keepTree: boolean;
|
|
4842
4809
|
slug: string;
|
|
4843
4810
|
t0: number;
|
|
4844
4811
|
facts: RepoFacts;
|
|
@@ -4850,8 +4817,14 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4850
4817
|
rebound?: Rebound;
|
|
4851
4818
|
rebindRefused?: RebindRefused;
|
|
4852
4819
|
}): Promise<AttachOk | ThreadErr> {
|
|
4853
|
-
const { threadKey, refHint, wantSha, reuse,
|
|
4820
|
+
const { threadKey, refHint, wantSha, reuse, slug, t0, facts, record, mode, rollback } = input;
|
|
4854
4821
|
const { rebound, rebindRefused } = input;
|
|
4822
|
+
// Reassigned once, under the lock, when the bound ref turns out gone from
|
|
4823
|
+
// the mirror and the binding goes back to the default (item 16's second
|
|
4824
|
+
// movement): everything after the lock — deps, credentials, the row's
|
|
4825
|
+
// final write, the answer — then speaks of the returned binding.
|
|
4826
|
+
let binding = input.binding;
|
|
4827
|
+
let returned: Returned | undefined;
|
|
4855
4828
|
|
|
4856
4829
|
// Command-level token mint — before the lock so mint latency
|
|
4857
4830
|
// never holds the mutex, and failure never blocks the attach.
|
|
@@ -4882,7 +4855,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4882
4855
|
// The expected head applies to the ref it was resolved for; a sticky
|
|
4883
4856
|
// binding on another branch drops it (item 51) rather than fetching on
|
|
4884
4857
|
// every attach of a thread that can never be at that commit.
|
|
4885
|
-
|
|
4858
|
+
let want = wantShaForBinding({ boundRef: binding.ref, refHint, wantSha });
|
|
4886
4859
|
let fetchToken: string | null = token;
|
|
4887
4860
|
if (!fetchToken && githubAppConfigured(this.env) && (await this.mirrorNeedsFetchFor(binding.ref, want))) {
|
|
4888
4861
|
fetchToken = (await mintRepoScopedToken(this.env, slug).catch(() => null))?.token ?? null;
|
|
@@ -4912,11 +4885,27 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4912
4885
|
// for every missing ref, so only a fetched mirror needs the re-read.
|
|
4913
4886
|
// (Under the mirror mutex — the mirror cannot change in between.)
|
|
4914
4887
|
const refExists = !fetched || (await this.refExists(binding.ref));
|
|
4915
|
-
|
|
4888
|
+
let target = attachTarget({
|
|
4916
4889
|
refExists,
|
|
4917
4890
|
wantSha: want,
|
|
4918
4891
|
commitInMirror: !refExists && want !== null && (await this.commitInMirror(want)),
|
|
4919
4892
|
});
|
|
4893
|
+
if (target.kind === "unknown-ref" && canReturnToDefault(binding, facts.defaultRef)) {
|
|
4894
|
+
// The branch a rebind moved this thread onto is gone even after the
|
|
4895
|
+
// fetch — deleted once its pull request merged. The binding goes
|
|
4896
|
+
// back to the default (item 16's second movement) and the attach
|
|
4897
|
+
// goes on there: the default is always in the mirror, and the
|
|
4898
|
+
// worktree step below provisions the tree at its tip.
|
|
4899
|
+
const back = await this.returnBindingToDefault(binding, facts.defaultRef);
|
|
4900
|
+
binding = back.binding;
|
|
4901
|
+
returned = back.returned;
|
|
4902
|
+
want = wantShaForBinding({ boundRef: binding.ref, refHint, wantSha });
|
|
4903
|
+
target = attachTarget({
|
|
4904
|
+
refExists: await this.refExists(binding.ref),
|
|
4905
|
+
wantSha: want,
|
|
4906
|
+
commitInMirror: false,
|
|
4907
|
+
});
|
|
4908
|
+
}
|
|
4920
4909
|
if (target.kind === "unknown-ref") {
|
|
4921
4910
|
throw new StepError(
|
|
4922
4911
|
"unknown-ref",
|
|
@@ -4929,7 +4918,6 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4929
4918
|
const recreated = await this.ensureThreadWorktree(binding, sha, mode.originUrl, mode.modeSwitch, {
|
|
4930
4919
|
detached: target.kind === "sha",
|
|
4931
4920
|
reuse,
|
|
4932
|
-
keepTree,
|
|
4933
4921
|
});
|
|
4934
4922
|
return { sha, threadLockKey, recreated };
|
|
4935
4923
|
}, ATTACH_MUTEX_WAIT_MS);
|
|
@@ -5027,20 +5015,20 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5027
5015
|
trace: this.currentSteps(),
|
|
5028
5016
|
...(rebound !== undefined ? { rebound } : {}),
|
|
5029
5017
|
...(rebindRefused !== undefined ? { rebindRefused } : {}),
|
|
5018
|
+
...(returned !== undefined ? { returned } : {}),
|
|
5030
5019
|
};
|
|
5031
5020
|
}
|
|
5032
5021
|
|
|
5033
5022
|
/** Ensure the worktree exists. A provisioning attach wipes dirty/stale
|
|
5034
|
-
* trees
|
|
5035
|
-
*
|
|
5036
|
-
*
|
|
5037
|
-
*
|
|
5038
|
-
*
|
|
5039
|
-
*
|
|
5040
|
-
*
|
|
5041
|
-
* the
|
|
5042
|
-
*
|
|
5043
|
-
* holding the mirror mutex: the clone reads the mirror.
|
|
5023
|
+
* trees — a run starts from a clean tree at the bound ref's tip (item 17),
|
|
5024
|
+
* and a rebind that moved the binding leaves a tree on the old branch that
|
|
5025
|
+
* this discipline recreates like any stale one; a reusing attach
|
|
5026
|
+
* (`opts.reuse`, item 66) keeps a readable tree exactly as it stands (the
|
|
5027
|
+
* dirt and the HEAD are the resumed run's own work) and throws
|
|
5028
|
+
* `ReuseRefusedError` for one it cannot keep, touching nothing. The
|
|
5029
|
+
* decision is the pure `decideWorktree`; this method measures the facts
|
|
5030
|
+
* and runs the verdict. Returns true when the tree was (re)created. MUST be
|
|
5031
|
+
* called holding the mirror mutex: the clone reads the mirror.
|
|
5044
5032
|
*
|
|
5045
5033
|
* "Dirty" is tracked-file dirt (`status --porcelain -uno`): untracked
|
|
5046
5034
|
* scratch files are the thread's own state and survive re-attach.
|
|
@@ -5063,11 +5051,8 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5063
5051
|
/** `detached`: the bound ref is gone from the mirror and `sha` is the
|
|
5064
5052
|
* expected commit it still holds (item 51) — the tree is checked out at
|
|
5065
5053
|
* that commit, detached, instead of at a branch. `reuse`: a resumed
|
|
5066
|
-
* run's attach (item 66): keep the tree as it stands, or refuse.
|
|
5067
|
-
|
|
5068
|
-
* tree already on the branch (item 16): keep a readable tree as it
|
|
5069
|
-
* stands, provision one that is not there. */
|
|
5070
|
-
opts: { detached: boolean; reuse: boolean; keepTree: boolean } = { detached: false, reuse: false, keepTree: false },
|
|
5054
|
+
* run's attach (item 66): keep the tree as it stands, or refuse. */
|
|
5055
|
+
opts: { detached: boolean; reuse: boolean } = { detached: false, reuse: false },
|
|
5071
5056
|
): Promise<boolean> {
|
|
5072
5057
|
const wt = binding.worktreePath;
|
|
5073
5058
|
const threadDir = parentDir(wt);
|
|
@@ -5104,14 +5089,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5104
5089
|
}
|
|
5105
5090
|
}
|
|
5106
5091
|
}
|
|
5107
|
-
const decision = decideWorktree({
|
|
5108
|
-
reuse: opts.reuse,
|
|
5109
|
-
keepTree: opts.keepTree,
|
|
5110
|
-
modeSwitch,
|
|
5111
|
-
sha,
|
|
5112
|
-
worktreePath: wt,
|
|
5113
|
-
facts,
|
|
5114
|
-
});
|
|
5092
|
+
const decision = decideWorktree({ reuse: opts.reuse, modeSwitch, sha, worktreePath: wt, facts });
|
|
5115
5093
|
if (decision.kind === "refuse") throw new ReuseRefusedError(decision.why);
|
|
5116
5094
|
if (decision.kind === "reuse") return false;
|
|
5117
5095
|
|
|
@@ -6092,19 +6070,21 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6092
6070
|
|
|
6093
6071
|
/** POST /detach: a run has ended — give the thread's pool user back now
|
|
6094
6072
|
* instead of holding it until the TTL sweep (the pool is sized for
|
|
6095
|
-
* simultaneous runs).
|
|
6096
|
-
*
|
|
6097
|
-
*
|
|
6098
|
-
* the
|
|
6099
|
-
*
|
|
6100
|
-
*
|
|
6101
|
-
*
|
|
6073
|
+
* simultaneous runs). A run starts from a clean tree (item 17), so the tree
|
|
6074
|
+
* goes whatever it holds: what it held — uncommitted changes, unpushed
|
|
6075
|
+
* commits — is measured once, as the thread user, and named in the answer
|
|
6076
|
+
* (`leftBehind`) so the loss is never silent. The one thing that keeps a
|
|
6077
|
+
* tree is an op still in flight in it (`busy`) — `force` (read-only agents,
|
|
6078
|
+
* hard stops) KILLS that op first (the bot has already dropped its fetch,
|
|
6079
|
+
* the command would otherwise run on and hold the user until the sweep)
|
|
6080
|
+
* and measures nothing. No binding → 404-shaped error; already evicted → a
|
|
6081
|
+
* no-op success. Never flips lifecycle state. */
|
|
6102
6082
|
async detachThread(
|
|
6103
6083
|
threadKey: string,
|
|
6104
6084
|
force: boolean,
|
|
6105
6085
|
/** What the ending run pushed (item 16a): remembered on the binding first, whatever the detach then decides. */
|
|
6106
6086
|
pushed: readonly PushedBranch[] = [],
|
|
6107
|
-
): Promise<
|
|
6087
|
+
): Promise<DetachAnswer | ThreadErr> {
|
|
6108
6088
|
if (pushed.length > 0) await this.rememberOwnBranches(threadKey, pushed);
|
|
6109
6089
|
const binding = await this.ctx.storage.get<ThreadBinding>(threadBindingKey(threadKey));
|
|
6110
6090
|
if (!binding) return { error: `no-binding: ${threadKey} has never attached to this resident`, status: 404 };
|
|
@@ -6125,11 +6105,18 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6125
6105
|
if (left > 0) return { released: false, reason: busyAfterKillReason(left), user: binding.user };
|
|
6126
6106
|
}
|
|
6127
6107
|
const active = await this.isRuntimeActive().catch(() => false);
|
|
6108
|
+
// What the tree still holds, for the answer — never a reason to keep it.
|
|
6109
|
+
// Not measured on a force release: a read-only tree holds nothing, and a
|
|
6110
|
+
// hard stop's tree is whatever the killed command left. A probe that
|
|
6111
|
+
// fails names nothing (never a guess) and is a log line.
|
|
6112
|
+
let leftBehind: LeftBehind | undefined;
|
|
6128
6113
|
if (!force && active) {
|
|
6129
|
-
const
|
|
6130
|
-
|
|
6114
|
+
const measured = await this.worktreeCleanliness(binding);
|
|
6115
|
+
leftBehind = leftBehindOf(measured);
|
|
6116
|
+
if (!measured.clean && leftBehind === undefined)
|
|
6117
|
+
console.log(`detach: ${threadKey}: the tree could not be measured before its release (${measured.reason})`);
|
|
6131
6118
|
}
|
|
6132
|
-
// Re-check right before removal: the
|
|
6119
|
+
// Re-check right before removal: the measurement above awaited (the DO
|
|
6133
6120
|
// yields at each await), so an exec that arrived mid-detach would otherwise
|
|
6134
6121
|
// have its tree removed under it.
|
|
6135
6122
|
const busyNow = this.threadOpsInFlight.get(threadKey) ?? 0;
|
|
@@ -6139,22 +6126,27 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6139
6126
|
reason: `busy: ${busyNow} operation(s) started during detach — kept`,
|
|
6140
6127
|
user: binding.user,
|
|
6141
6128
|
};
|
|
6142
|
-
// Same re-read as the sweep: a re-attach during the
|
|
6129
|
+
// Same re-read as the sweep: a re-attach during the measurement means a
|
|
6143
6130
|
// fresh tree we must not remove from a stale snapshot.
|
|
6144
6131
|
const current = await this.ctx.storage.get<ThreadBinding>(threadBindingKey(threadKey));
|
|
6145
6132
|
if (!current || current.evicted) return { released: false, reason: "already-evicted" };
|
|
6146
6133
|
if (current.lastAttachAt !== binding.lastAttachAt)
|
|
6147
|
-
return { released: false, reason: "re-attached during the
|
|
6134
|
+
return { released: false, reason: "re-attached during the detach — kept", user: current.user };
|
|
6148
6135
|
const user = current.user;
|
|
6149
|
-
// Same as the sweep: `active` was read before the
|
|
6136
|
+
// Same as the sweep: `active` was read before the measurement's awaits; a
|
|
6150
6137
|
// container that woke meanwhile must get the rm, not an orphaned tree.
|
|
6151
6138
|
const activeNow = await this.isRuntimeActive().catch(() => false);
|
|
6152
6139
|
if (!(await this.evictBinding(current, activeNow, `detach`, "detach"))) {
|
|
6153
6140
|
return { released: false, reason: "re-attached during eviction — kept", user };
|
|
6154
6141
|
}
|
|
6142
|
+
if (leftBehind) {
|
|
6143
|
+
console.log(
|
|
6144
|
+
`detach: ${threadKey} released — left behind ${leftBehind.uncommittedChanges} uncommitted change(s) and ${leftBehind.unpushedCommits} unpushed commit(s), discarded with the tree`,
|
|
6145
|
+
);
|
|
6146
|
+
}
|
|
6155
6147
|
// Item 55: the tree is gone; the gauge catches up at the next refresh
|
|
6156
6148
|
// instance's `measure` step, and the admission's `df` sees the space now.
|
|
6157
|
-
return { released: true, user };
|
|
6149
|
+
return { released: true, user, ...(leftBehind !== undefined ? { leftBehind } : {}) };
|
|
6158
6150
|
}
|
|
6159
6151
|
|
|
6160
6152
|
/** Force-detach's kill: end every process owned by the pool user —
|
|
@@ -6254,35 +6246,29 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6254
6246
|
const ttlDays = record?.worktreeTtlDays ?? WORKTREE_TTL_DAYS_DEFAULT;
|
|
6255
6247
|
const cutoff = systemClock() - ttlDays * 86_400_000;
|
|
6256
6248
|
const all = await this.ctx.storage.list<ThreadBinding>({ prefix: THREAD_KEY_PREFIX });
|
|
6257
|
-
const active = await this.isRuntimeActive().catch(() => false);
|
|
6258
6249
|
const idleCutoff = systemClock() - CLEAN_IDLE_RELEASE_S * 1000;
|
|
6259
6250
|
for (const binding of all.values()) {
|
|
6260
6251
|
if (binding.evicted || !binding.user) continue;
|
|
6261
6252
|
const last = Date.parse(binding.lastAttachAt);
|
|
6262
6253
|
if (last >= cutoff) {
|
|
6263
|
-
// Not past the TTL. Still release it if it has been idle for an hour
|
|
6264
|
-
// nothing is running on it
|
|
6265
|
-
//
|
|
6266
|
-
//
|
|
6267
|
-
//
|
|
6268
|
-
//
|
|
6269
|
-
//
|
|
6270
|
-
//
|
|
6254
|
+
// Not past the TTL. Still release it if it has been idle for an hour
|
|
6255
|
+
// and nothing is running on it: the run that used it is over, and
|
|
6256
|
+
// whatever its tree holds has no future — the next attach provisions
|
|
6257
|
+
// a clean tree (item 17) — so there is nothing to keep it for. This is
|
|
6258
|
+
// what drains the bindings of runs whose release never came (a
|
|
6259
|
+
// resident that was sick at the run's end, a run older than
|
|
6260
|
+
// `/detach`). A live run is protected by its op in flight, not by its
|
|
6261
|
+
// dirt. A slept container has no tree any more anyway (sleep destroys
|
|
6262
|
+
// the disk).
|
|
6271
6263
|
const busy = this.threadOpsInFlight.get(binding.threadKey) ?? 0;
|
|
6272
|
-
|
|
6273
|
-
last < idleCutoff && busy === 0 && (!active || (await this.worktreeCleanliness(binding)).clean);
|
|
6274
|
-
// Re-read right before removal: the clean check awaited (the DO
|
|
6275
|
-
// yields), so an exec that arrived meanwhile would otherwise have
|
|
6276
|
-
// its tree removed under it — same guard as detachThread.
|
|
6277
|
-
const busyNow = this.threadOpsInFlight.get(binding.threadKey) ?? 0;
|
|
6278
|
-
if (!cleanIdle || busyNow > 0) {
|
|
6264
|
+
if (last >= idleCutoff || busy > 0) {
|
|
6279
6265
|
kept++;
|
|
6280
6266
|
continue;
|
|
6281
6267
|
}
|
|
6282
6268
|
}
|
|
6283
|
-
// Re-read the binding
|
|
6284
|
-
//
|
|
6285
|
-
// from this loop's stale snapshot would rm the fresh tree.
|
|
6269
|
+
// Re-read the binding: earlier iterations awaited (the DO yields), so a
|
|
6270
|
+
// re-attach that completed meanwhile bumped lastAttachAt and rebuilt the
|
|
6271
|
+
// tree — evicting from this loop's stale snapshot would rm the fresh tree.
|
|
6286
6272
|
const current = await this.ctx.storage.get<ThreadBinding>(threadBindingKey(binding.threadKey));
|
|
6287
6273
|
if (!current || current.evicted || current.lastAttachAt !== binding.lastAttachAt) {
|
|
6288
6274
|
kept++;
|