@centralu/host-linux-x64 0.1.0-beta.22 → 0.1.0-beta.23
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/host/bundle-info.json +2 -2
- package/host/codex-orchestrator-bridge.mjs +3 -1
- package/host/main.mjs +1759 -276
- package/package.json +1 -1
package/host/main.mjs
CHANGED
|
@@ -42960,12 +42960,12 @@ function refuse(message, io2 = realIo) {
|
|
|
42960
42960
|
import { parseArgs } from "node:util";
|
|
42961
42961
|
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
42962
42962
|
import { homedir as homedir14, hostname as hostname4 } from "node:os";
|
|
42963
|
-
import { dirname as
|
|
42963
|
+
import { dirname as dirname12, join as join34 } from "node:path";
|
|
42964
42964
|
import { mkdirSync as mkdirSync11, writeSync as writeSync4 } from "node:fs";
|
|
42965
42965
|
|
|
42966
42966
|
// packages/protocol/src/brand.ts
|
|
42967
42967
|
var APP_NAME = "Centralu";
|
|
42968
|
-
var APP_VERSION = "0.1.0-beta.
|
|
42968
|
+
var APP_VERSION = "0.1.0-beta.23";
|
|
42969
42969
|
var APP_SLUG = "centralu";
|
|
42970
42970
|
var CLIENT_INFO = { name: APP_SLUG, title: APP_NAME, version: APP_VERSION };
|
|
42971
42971
|
var DATA_DIR = ".centralu";
|
|
@@ -43015,6 +43015,13 @@ function split(raw) {
|
|
|
43015
43015
|
const pre2 = dash === -1 ? null : v.slice(dash + 1);
|
|
43016
43016
|
return [core, pre2];
|
|
43017
43017
|
}
|
|
43018
|
+
var LOCAL_BUILD = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*\.local\.[1-9]\d*$/;
|
|
43019
|
+
function isLocalBuildVersion(v) {
|
|
43020
|
+
return typeof v === "string" && v.length <= 64 && LOCAL_BUILD.test(v);
|
|
43021
|
+
}
|
|
43022
|
+
function keepsInstalledCopy(target, copyVersion) {
|
|
43023
|
+
return isLocalBuildVersion(copyVersion) && isNewerVersion(copyVersion, target);
|
|
43024
|
+
}
|
|
43018
43025
|
|
|
43019
43026
|
// packages/protocol/src/agent-version.ts
|
|
43020
43027
|
function parseCliVersion(text2) {
|
|
@@ -58020,7 +58027,12 @@ var ApprovalDetail = external_exports.discriminatedUnion("kind", [
|
|
|
58020
58027
|
diffPreview: external_exports.string(),
|
|
58021
58028
|
multi: external_exports.boolean().default(false)
|
|
58022
58029
|
}),
|
|
58023
|
-
|
|
58030
|
+
/**
|
|
58031
|
+
* Anything else, as one text to read. `about` names a card the host raised that a window may know more about
|
|
58032
|
+
* (#665: `centralu_apply`, an agent applying the installed update); a window that does not know it, or predates
|
|
58033
|
+
* the field, reads `raw` as it always has, so the question is the whole of the card in every build.
|
|
58034
|
+
*/
|
|
58035
|
+
external_exports.object({ kind: external_exports.literal("other"), raw: external_exports.string(), about: external_exports.string().optional() }),
|
|
58024
58036
|
external_exports.object({
|
|
58025
58037
|
kind: external_exports.literal("capability"),
|
|
58026
58038
|
/** The app trying to use the capability — name is from the manifest */
|
|
@@ -58076,7 +58088,11 @@ var ApprovalDetail = external_exports.discriminatedUnion("kind", [
|
|
|
58076
58088
|
/** One line for a person to read: the task, cut short */
|
|
58077
58089
|
text: external_exports.string()
|
|
58078
58090
|
}),
|
|
58079
|
-
/**
|
|
58091
|
+
/**
|
|
58092
|
+
* An ordinary session asks to check or install Centralu itself (#581). Applying the installed build (#665) is not an
|
|
58093
|
+
* action here: a released window parses events strictly and drops one whose enum it does not know, so that card
|
|
58094
|
+
* rides on `other` with `about: 'centralu_apply'`.
|
|
58095
|
+
*/
|
|
58080
58096
|
external_exports.object({
|
|
58081
58097
|
kind: external_exports.literal("centralu_update"),
|
|
58082
58098
|
action: external_exports.enum(["check", "install"])
|
|
@@ -58101,6 +58117,11 @@ var ApprovalDetail = external_exports.discriminatedUnion("kind", [
|
|
|
58101
58117
|
machine: external_exports.string().optional()
|
|
58102
58118
|
})
|
|
58103
58119
|
]);
|
|
58120
|
+
var CENTRALU_APPLY = "centralu_apply";
|
|
58121
|
+
var CENTRALU_APPLY_QUESTION = "Allow this session to apply the installed Centralu update? The window relaunches into it and running agents keep going. Right after, the new window switches the agent host to the new build: in any session, a Centralu tool call still running after 10 seconds (an agent waiting on its child agents, for one) is stopped and has to be tried again.";
|
|
58122
|
+
function isCentraluApply(d2) {
|
|
58123
|
+
return d2.kind === "other" && d2.about === CENTRALU_APPLY;
|
|
58124
|
+
}
|
|
58104
58125
|
var ProjectConsent = external_exports.object({
|
|
58105
58126
|
fromProjectId: external_exports.string(),
|
|
58106
58127
|
fromName: external_exports.string(),
|
|
@@ -58537,7 +58558,15 @@ var UiPreferences = external_exports.object({
|
|
|
58537
58558
|
*/
|
|
58538
58559
|
sessionTools: external_exports.boolean(),
|
|
58539
58560
|
/** Running children per project: null uses preset defaults (auto unlimited, otherwise 4); 0 is unlimited. */
|
|
58540
|
-
agentCeiling: external_exports.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).nullable().optional()
|
|
58561
|
+
agentCeiling: external_exports.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).nullable().optional(),
|
|
58562
|
+
/**
|
|
58563
|
+
* The sidebar groups the person folded (#666): the children of a session (`session:<id>`) and the
|
|
58564
|
+
* sessions of an app (`app:<key>`). Only folded groups are listed, so a group nobody touched is
|
|
58565
|
+
* open and an unknown or stale key does nothing. A way of looking, kept with the other screen
|
|
58566
|
+
* preferences so it survives a restart; the patch carries the whole list, since a list cannot be
|
|
58567
|
+
* patched by one entry.
|
|
58568
|
+
*/
|
|
58569
|
+
foldedGroups: external_exports.array(external_exports.string().max(300)).max(500)
|
|
58541
58570
|
});
|
|
58542
58571
|
var UiPreferencesPatch = UiPreferences.partial();
|
|
58543
58572
|
var DEFAULT_UI_PREFERENCES = {
|
|
@@ -58550,7 +58579,8 @@ var DEFAULT_UI_PREFERENCES = {
|
|
|
58550
58579
|
codeFont: "",
|
|
58551
58580
|
lineHeight: "normal",
|
|
58552
58581
|
textSize: 1,
|
|
58553
|
-
sessionTools: true
|
|
58582
|
+
sessionTools: true,
|
|
58583
|
+
foldedGroups: []
|
|
58554
58584
|
};
|
|
58555
58585
|
function parseUiPreferences(raw) {
|
|
58556
58586
|
const out = { ...DEFAULT_UI_PREFERENCES };
|
|
@@ -58812,7 +58842,17 @@ var NormalizedEvent = external_exports.discriminatedUnion("type", [
|
|
|
58812
58842
|
* only came in lists and in answers to the window that woke a session, so another window, or a session woken by the
|
|
58813
58843
|
* orchestrator or an app, kept showing the old value.
|
|
58814
58844
|
*/
|
|
58815
|
-
external_exports.object({
|
|
58845
|
+
external_exports.object({
|
|
58846
|
+
...base,
|
|
58847
|
+
type: external_exports.literal("session_live"),
|
|
58848
|
+
live: external_exports.boolean(),
|
|
58849
|
+
/**
|
|
58850
|
+
* Why the process went away, when `live` is false (#568, #648): `asleep` (an idle session put to sleep),
|
|
58851
|
+
* `restart_failed` (the host replaced the process and the new one did not come up), `crashed` (the process ended
|
|
58852
|
+
* on its own). Absent when `live` is true.
|
|
58853
|
+
*/
|
|
58854
|
+
reason: external_exports.enum(["asleep", "restart_failed", "crashed"]).optional()
|
|
58855
|
+
}),
|
|
58816
58856
|
external_exports.object({
|
|
58817
58857
|
...base,
|
|
58818
58858
|
type: external_exports.literal("usage_update"),
|
|
@@ -58982,6 +59022,11 @@ var NormalizedEvent = external_exports.discriminatedUnion("type", [
|
|
|
58982
59022
|
verbosity: external_exports.string().nullable(),
|
|
58983
59023
|
/** Response speed. Follows the same snapshot rule as the three above — optional since old frames lack it */
|
|
58984
59024
|
serviceTier: external_exports.string().nullable().optional(),
|
|
59025
|
+
/**
|
|
59026
|
+
* The permission preset after the change (#659). A snapshot like the four above; absent on frames that
|
|
59027
|
+
* predate it and on a tool's own switch, which never touches it. The receiving side keeps its value then.
|
|
59028
|
+
*/
|
|
59029
|
+
permissionPreset: PermissionPreset.optional(),
|
|
58985
59030
|
/**
|
|
58986
59031
|
* Whose hand it was (#304). Absent means the orchestrator, the only author this event had before.
|
|
58987
59032
|
*
|
|
@@ -58992,7 +59037,18 @@ var NormalizedEvent = external_exports.discriminatedUnion("type", [
|
|
|
58992
59037
|
* From an adapter, the snapshot is the adapter's own view; the host applies only the fields that differ from what
|
|
58993
59038
|
* the process was launched with, and re-sends its full snapshot.
|
|
58994
59039
|
*/
|
|
58995
|
-
by: external_exports.enum(["orchestrator", "tool"]).optional()
|
|
59040
|
+
by: external_exports.enum(["orchestrator", "tool"]).optional(),
|
|
59041
|
+
/**
|
|
59042
|
+
* A client changed it through `agents.updateSettings` (#659). The caller already has the answer, but every other
|
|
59043
|
+
* window, linked machine and the person's own window (when an agent made the call) only learns it from this event.
|
|
59044
|
+
* It updates the values shown without the "Orchestrator changed" toast: nobody else's hand was involved, and the
|
|
59045
|
+
* caller's own toast states what it did.
|
|
59046
|
+
*
|
|
59047
|
+
* **A separate flag, not a third `by` word**: a build from before it parses this frame against a `by` enum without
|
|
59048
|
+
* the new word, and a frame that fails to parse is dropped whole. As a flag it is stripped as an unknown key, and
|
|
59049
|
+
* that build still updates the values (and raises one toast, as it did for any change not made by a tool).
|
|
59050
|
+
*/
|
|
59051
|
+
person: external_exports.boolean().optional()
|
|
58996
59052
|
}),
|
|
58997
59053
|
external_exports.object({ ...base, type: external_exports.literal("history_synced"), added: external_exports.number() }),
|
|
58998
59054
|
/** The session was deleted — the list must stay correct in other windows and after a reconnect too */
|
|
@@ -59052,7 +59108,13 @@ var NormalizedEvent = external_exports.discriminatedUnion("type", [
|
|
|
59052
59108
|
type: external_exports.literal("background_tasks"),
|
|
59053
59109
|
live: external_exports.array(BackgroundTask),
|
|
59054
59110
|
ended: external_exports.array(BackgroundTask).optional(),
|
|
59055
|
-
clearEnded: external_exports.boolean().optional()
|
|
59111
|
+
clearEnded: external_exports.boolean().optional(),
|
|
59112
|
+
/**
|
|
59113
|
+
* `live` is the tool's own full list of what runs in the process (#648): Claude's `background_tasks_changed`. Only
|
|
59114
|
+
* such a list settles what a process adopted from a previous host holds; a level built from what this host saw
|
|
59115
|
+
* (an ending, a `/loop` timer) does not.
|
|
59116
|
+
*/
|
|
59117
|
+
snapshot: external_exports.boolean().optional()
|
|
59056
59118
|
}),
|
|
59057
59119
|
/**
|
|
59058
59120
|
* The version of the agent CLI this session's process runs (#297): Claude's init message (`claude_code_version`), the
|
|
@@ -59080,6 +59142,18 @@ var NormalizedEvent = external_exports.discriminatedUnion("type", [
|
|
|
59080
59142
|
* the long-running window the schedule was for.
|
|
59081
59143
|
*/
|
|
59082
59144
|
external_exports.object({ ...appScoped, type: external_exports.literal("update_status"), status: UpdateStatus }),
|
|
59145
|
+
/**
|
|
59146
|
+
* An agent asked to apply the installed update (#665, `update_centralu` action "apply"): each window that hears this
|
|
59147
|
+
* makes the checks its "Apply now" makes and answers with `updates.answerApply`. Nothing relaunches yet; a window
|
|
59148
|
+
* the host picks is told so by `update_apply_go`. A replay of this event after the host stopped asking is answered
|
|
59149
|
+
* with no ticket, so it starts nothing.
|
|
59150
|
+
*/
|
|
59151
|
+
external_exports.object({ ...appScoped, type: external_exports.literal("update_apply_request"), requestId: external_exports.string() }),
|
|
59152
|
+
/**
|
|
59153
|
+
* The one window holding `ticket` (from its `updates.answerApply`) relaunches now, through the same path as its
|
|
59154
|
+
* "Apply now" button, and reports with `updates.reportApply`. Every other window ignores it: only one relaunches.
|
|
59155
|
+
*/
|
|
59156
|
+
external_exports.object({ ...appScoped, type: external_exports.literal("update_apply_go"), requestId: external_exports.string(), ticket: external_exports.string() }),
|
|
59083
59157
|
/**
|
|
59084
59158
|
* An external app's tool call finished (M4 A-4). Deliberately coarse: it never carries what
|
|
59085
59159
|
* changed, only that the receiving side should refetch. An external app's state lives inside the
|
|
@@ -59486,7 +59560,13 @@ var UpdateSettingsParams = external_exports.object({
|
|
|
59486
59560
|
verbosity: external_exports.string().nullable().optional(),
|
|
59487
59561
|
/** Response speed. Same rule — which tiers are supported is stated by the model list */
|
|
59488
59562
|
serviceTier: external_exports.string().nullable().optional(),
|
|
59489
|
-
permissionPreset: PermissionPreset.optional()
|
|
59563
|
+
permissionPreset: PermissionPreset.optional(),
|
|
59564
|
+
/**
|
|
59565
|
+
* Whose hand makes the call (#659). Absent means a person's client, which is announced without a toast. The
|
|
59566
|
+
* orchestrator's settings tool on a linked machine sends `orchestrator`, so the machine broadcasts the change as the
|
|
59567
|
+
* orchestrator's and the window still toasts it. A host from before this field strips it, and then says nothing.
|
|
59568
|
+
*/
|
|
59569
|
+
by: external_exports.literal("orchestrator").optional()
|
|
59490
59570
|
});
|
|
59491
59571
|
var SessionKind = external_exports.enum(["worker", "orchestrator", "coordinator"]);
|
|
59492
59572
|
var SessionInfo = external_exports.object({
|
|
@@ -61158,6 +61238,24 @@ var RpcMethods = {
|
|
|
61158
61238
|
params: external_exports.object({}),
|
|
61159
61239
|
result: UpdateStatus
|
|
61160
61240
|
},
|
|
61241
|
+
/**
|
|
61242
|
+
* A window's answer to `update_apply_request` (#665): whether its "Apply now" could relaunch into the installed build
|
|
61243
|
+
* right now, and why not. The first window that is ready gets a ticket; every other answer, and any answer after the
|
|
61244
|
+
* host stopped asking, gets null.
|
|
61245
|
+
*/
|
|
61246
|
+
"updates.answerApply": {
|
|
61247
|
+
params: external_exports.object({ requestId: external_exports.string(), ready: external_exports.boolean(), reason: external_exports.string().optional(), version: external_exports.string().optional() }),
|
|
61248
|
+
result: external_exports.object({ ticket: external_exports.string().nullable() })
|
|
61249
|
+
},
|
|
61250
|
+
/**
|
|
61251
|
+
* The picked window's outcome (#665): `started` right before it relaunches, then with `error` when it did not, or
|
|
61252
|
+
* with neither once the shell accepted the relaunch. A relaunching window may close before that last report; the
|
|
61253
|
+
* host reads its connection ending as the relaunch only after `started`.
|
|
61254
|
+
*/
|
|
61255
|
+
"updates.reportApply": {
|
|
61256
|
+
params: external_exports.object({ requestId: external_exports.string(), ticket: external_exports.string(), started: external_exports.boolean().optional(), error: external_exports.string().optional() }),
|
|
61257
|
+
result: external_exports.object({})
|
|
61258
|
+
},
|
|
61161
61259
|
/**
|
|
61162
61260
|
* Reads screen settings (UiPreferences).
|
|
61163
61261
|
*
|
|
@@ -62711,6 +62809,10 @@ var HostServer = class _HostServer {
|
|
|
62711
62809
|
const frame = `{"kind":"event","seq":${entry.seq},"event":${body}}`;
|
|
62712
62810
|
for (const ws2 of this.clients) this.sendTo(ws2, frame);
|
|
62713
62811
|
}
|
|
62812
|
+
/** The connections that said hello with the front door's token: windows, and a hub linked to this host. Never a bridge */
|
|
62813
|
+
get clientCount() {
|
|
62814
|
+
return this.clients.size;
|
|
62815
|
+
}
|
|
62714
62816
|
/** This host lifetime's id — sent in every `hello_ok` (#82) */
|
|
62715
62817
|
get streamEpoch() {
|
|
62716
62818
|
return this.log.streamEpoch;
|
|
@@ -63027,15 +63129,15 @@ function buildViewCsp(csp) {
|
|
|
63027
63129
|
const frame = sanitizeDomains(csp?.frameDomains);
|
|
63028
63130
|
const base2 = sanitizeDomains(csp?.baseUriDomains);
|
|
63029
63131
|
const r = resource.kept.join(" ");
|
|
63030
|
-
const
|
|
63132
|
+
const join35 = (...parts) => parts.filter(Boolean).join(" ");
|
|
63031
63133
|
const policy = [
|
|
63032
63134
|
"default-src 'none'",
|
|
63033
|
-
`script-src ${
|
|
63034
|
-
`style-src ${
|
|
63035
|
-
`img-src ${
|
|
63036
|
-
`font-src ${
|
|
63037
|
-
`media-src ${
|
|
63038
|
-
`worker-src ${
|
|
63135
|
+
`script-src ${join35("'unsafe-inline'", r)}`,
|
|
63136
|
+
`style-src ${join35("'unsafe-inline'", r)}`,
|
|
63137
|
+
`img-src ${join35("data: blob:", r)}`,
|
|
63138
|
+
`font-src ${join35("data:", r)}`,
|
|
63139
|
+
`media-src ${join35("data: blob:", r)}`,
|
|
63140
|
+
`worker-src ${join35("blob:", r)}`,
|
|
63039
63141
|
`connect-src ${connect2.kept.length ? connect2.kept.join(" ") : "'none'"}`,
|
|
63040
63142
|
`frame-src ${frame.kept.length ? frame.kept.join(" ") : "'none'"}`,
|
|
63041
63143
|
"form-action 'none'",
|
|
@@ -64087,6 +64189,13 @@ function installedCopyPath(platform = HOST_PLATFORM, env = process.env, home = h
|
|
|
64087
64189
|
}
|
|
64088
64190
|
return posix2.join(home, ".local/share/applications", `${APP_SLUG}.desktop`);
|
|
64089
64191
|
}
|
|
64192
|
+
function installedCopyVersionSource(copy, platform = HOST_PLATFORM) {
|
|
64193
|
+
if (platform === "darwin") {
|
|
64194
|
+
return { kind: "command", file: "/usr/bin/plutil", args: ["-extract", "CFBundleShortVersionString", "raw", "-o", "-", posix2.join(copy, "Contents", "Info.plist")] };
|
|
64195
|
+
}
|
|
64196
|
+
if (platform === "win32") return { kind: "file", name: "centralu-version.txt" };
|
|
64197
|
+
return null;
|
|
64198
|
+
}
|
|
64090
64199
|
|
|
64091
64200
|
// packages/agent-host/src/dev-services/fs.ts
|
|
64092
64201
|
var MAX_TEXT = 2e6;
|
|
@@ -98208,7 +98317,7 @@ var OriginPorts = class {
|
|
|
98208
98317
|
// packages/agent-host/src/dev-services/worktree-recovery.ts
|
|
98209
98318
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
98210
98319
|
import { createReadStream, existsSync as existsSync13 } from "node:fs";
|
|
98211
|
-
import { chmod, lstat as lstat3, mkdir as mkdir2, mkdtemp, readFile, readdir as readdir2, realpath as realpath3, rm as rm2, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
|
|
98320
|
+
import { chmod, lstat as lstat3, mkdir as mkdir2, mkdtemp, readFile, readdir as readdir2, realpath as realpath3, rename as rename2, rm as rm2, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
|
|
98212
98321
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
98213
98322
|
import { join as join16, resolve as resolve2 } from "node:path";
|
|
98214
98323
|
var WORKTREE_RECOVERY_MAX_BYTES = 256 * 1024 * 1024;
|
|
@@ -98217,8 +98326,51 @@ function worktreeRecoveryDir(sessionId) {
|
|
|
98217
98326
|
if (!isSessionId(sessionId)) throw new Error("Invalid session id for worktree recovery");
|
|
98218
98327
|
return join16(dataRoot(), "worktree-recovery", sessionId);
|
|
98219
98328
|
}
|
|
98220
|
-
async function
|
|
98221
|
-
|
|
98329
|
+
async function stageWorktreeRecoveryRemoval(sessionId) {
|
|
98330
|
+
const dir = worktreeRecoveryDir(sessionId);
|
|
98331
|
+
if (!existsSync13(dir)) return { commit: async () => {
|
|
98332
|
+
}, abort: async () => {
|
|
98333
|
+
} };
|
|
98334
|
+
const staged = `${dir}${PURGING}${randomUUID6()}`;
|
|
98335
|
+
await rename2(dir, staged);
|
|
98336
|
+
return {
|
|
98337
|
+
commit: () => rm2(staged, { recursive: true, force: true, maxRetries: 3 }),
|
|
98338
|
+
abort: () => rename2(staged, dir)
|
|
98339
|
+
};
|
|
98340
|
+
}
|
|
98341
|
+
var PURGING = ".purging-";
|
|
98342
|
+
async function repairWorktreeRecoveryRemovals(trashed) {
|
|
98343
|
+
const root3 = join16(dataRoot(), "worktree-recovery");
|
|
98344
|
+
if (!existsSync13(root3)) return;
|
|
98345
|
+
for (const name of await readdir2(root3)) {
|
|
98346
|
+
const at2 = name.indexOf(PURGING);
|
|
98347
|
+
if (at2 === -1) continue;
|
|
98348
|
+
const sessionId = name.slice(0, at2);
|
|
98349
|
+
if (!isSessionId(sessionId)) continue;
|
|
98350
|
+
const staged = join16(root3, name);
|
|
98351
|
+
try {
|
|
98352
|
+
if (!trashed(sessionId)) {
|
|
98353
|
+
await rm2(staged, { recursive: true, force: true, maxRetries: 3 });
|
|
98354
|
+
continue;
|
|
98355
|
+
}
|
|
98356
|
+
const dir = join16(root3, sessionId);
|
|
98357
|
+
if (!existsSync13(dir)) {
|
|
98358
|
+
await rename2(staged, dir);
|
|
98359
|
+
continue;
|
|
98360
|
+
}
|
|
98361
|
+
for (const snapshot2 of await readdir2(staged)) {
|
|
98362
|
+
if (!existsSync13(join16(dir, snapshot2))) await rename2(join16(staged, snapshot2), join16(dir, snapshot2));
|
|
98363
|
+
}
|
|
98364
|
+
await rm2(staged, { recursive: true, force: true, maxRetries: 3 });
|
|
98365
|
+
} catch (e) {
|
|
98366
|
+
console.error(`[trash] recovery folder ${name} left by an interrupted purge was not repaired, retried at the next start: ${e.message}`);
|
|
98367
|
+
}
|
|
98368
|
+
}
|
|
98369
|
+
}
|
|
98370
|
+
var MARK = "refs/centralu/recovered/";
|
|
98371
|
+
async function clearRecoveredBranchMark(repo, branch) {
|
|
98372
|
+
if (!branch.startsWith("recovered/")) return;
|
|
98373
|
+
await runGit(repo, ["update-ref", "-d", `${MARK}${branch}`]);
|
|
98222
98374
|
}
|
|
98223
98375
|
async function* gitRecords(repo, args, input) {
|
|
98224
98376
|
const dir = await mkdtemp(join16(tmpdir2(), "cc-recovery-git-"));
|
|
@@ -98453,11 +98605,37 @@ async function restoreWorktreeRecovery(sessionId, repo, path2, branch, recovery,
|
|
|
98453
98605
|
}
|
|
98454
98606
|
if (!existsSync13(path2)) {
|
|
98455
98607
|
await rm2(join16(dir, "patches-applied"), { force: true });
|
|
98456
|
-
|
|
98457
|
-
|
|
98608
|
+
const tipOf = (ref) => runGit(repo, ["rev-parse", "--verify", ref]).then((s) => s.trim()).catch(() => null);
|
|
98609
|
+
const createdBy = (ref) => runGit(repo, ["log", "-g", "--format=%gs", ref, "--"]).then((s) => s.trim().split("\n").at(-1) ?? "").catch(() => "");
|
|
98610
|
+
const ours = async (name) => {
|
|
98611
|
+
const existing = await tipOf(`refs/heads/${name}`);
|
|
98612
|
+
if (!existing) return null;
|
|
98613
|
+
if (existing !== recovery.tip || await tipOf(`${MARK}${name}`) !== recovery.tip) return false;
|
|
98614
|
+
const made = await createdBy(`${MARK}${name}`);
|
|
98615
|
+
if (!made.startsWith("centralu recovery ") || made !== await createdBy(`refs/heads/${name}`)) return false;
|
|
98616
|
+
return (await runGit(repo, ["for-each-ref", "--format=%(worktreepath)", `refs/heads/${name}`])).trim() === "";
|
|
98617
|
+
};
|
|
98618
|
+
let create = true;
|
|
98619
|
+
if (await tipOf(`refs/heads/${branch}`)) {
|
|
98620
|
+
const base2 = `recovered/${sessionId}`;
|
|
98621
|
+
branch = base2;
|
|
98622
|
+
for (let suffix = 1; ; suffix++) {
|
|
98623
|
+
const state = await ours(branch);
|
|
98624
|
+
if (state !== false) {
|
|
98625
|
+
create = state === null;
|
|
98626
|
+
break;
|
|
98627
|
+
}
|
|
98628
|
+
branch = `${base2}-${suffix}`;
|
|
98629
|
+
}
|
|
98458
98630
|
}
|
|
98459
98631
|
if (root3.bundle) await runGit(repo, ["fetch", join16(dir, "0.bundle"), ...root3.refs ?? ["HEAD"]], { allowRepoPrograms: trust.trusted === true });
|
|
98460
|
-
|
|
98632
|
+
if (create && branch.startsWith("recovered/")) {
|
|
98633
|
+
const made = `centralu recovery ${randomUUID6()}`;
|
|
98634
|
+
await runGit(repo, ["update-ref", "--create-reflog", "-m", made, `${MARK}${branch}`, recovery.tip]);
|
|
98635
|
+
await runGit(repo, ["update-ref", "--create-reflog", "-m", made, `refs/heads/${branch}`, recovery.tip, ""]);
|
|
98636
|
+
create = false;
|
|
98637
|
+
}
|
|
98638
|
+
await runGit(repo, ["worktree", "add", ...create ? ["-b", branch, path2, recovery.tip] : [path2, branch]], { allowRepoPrograms: trust.trusted === true });
|
|
98461
98639
|
if (root3.patch || root3.staged) {
|
|
98462
98640
|
try {
|
|
98463
98641
|
if (root3.staged) await runGit(path2, ["apply", "--cached", "--binary", join16(dir, "0.staged.patch")]);
|
|
@@ -98580,11 +98758,17 @@ Ordinary checks follow npm, including dist-tag rollbacks. Installing reuses a su
|
|
|
98580
98758
|
from the last minute or checks again. Only while installing can a higher stored version seen less
|
|
98581
98759
|
than an hour ago win over the fresh tag. Older observations yield to npm. If the check fails, it
|
|
98582
98760
|
uses the stored version and says so. Checks preserve install errors until the next install attempt.
|
|
98761
|
+
An install asked for with nothing newer known checks first; nothing newer is then "Up to date", not
|
|
98762
|
+
a failure. The installed app is replaced by the npm release, even a higher released version, except
|
|
98763
|
+
a newer local build (<base>.local.N) waiting to be applied, which is kept; the line says so.
|
|
98583
98764
|
Once installed, the desktop app offers "Apply now": it relaunches into the new version, and running
|
|
98584
98765
|
agents, terminals and commands keep going. "Apply updates automatically when idle" (off by default)
|
|
98585
98766
|
installs a new version without a click and applies it once no session is working or waiting, no
|
|
98586
|
-
terminal or command is running, and the person is not typing.
|
|
98587
|
-
|
|
98767
|
+
terminal or command is running, and the person is not typing. An agent can ask for the same Apply now
|
|
98768
|
+
(update_centralu, action apply): under auto it applies, under normal the person is asked on a card,
|
|
98769
|
+
under safe it is refused, and it never applies what the button would not offer. With it on, a failed install is tried
|
|
98770
|
+
again after 10 minutes, then 1 hour, then every 6 hours; the error stays until the next attempt.
|
|
98771
|
+
From the terminal: \`centralu update\`. \`centralu install --force\` replaces a kept local build.`
|
|
98588
98772
|
};
|
|
98589
98773
|
function gist(description) {
|
|
98590
98774
|
const first = description.split(/ — |\. /)[0] ?? description;
|
|
@@ -98643,8 +98827,8 @@ function trustedJsonText(value) {
|
|
|
98643
98827
|
var ORCHESTRATOR_TOOLS = [
|
|
98644
98828
|
{
|
|
98645
98829
|
name: "update_centralu",
|
|
98646
|
-
description: "Checks for or installs a Centralu update.
|
|
98647
|
-
schema: external_exports.object({ action: external_exports.enum(["check", "install"]) })
|
|
98830
|
+
description: "Checks for or installs a Centralu update, or applies the installed one (the window relaunches; agents keep running). Never changes the person's automatic-update setting.",
|
|
98831
|
+
schema: external_exports.object({ action: external_exports.enum(["check", "install", "apply"]) })
|
|
98648
98832
|
},
|
|
98649
98833
|
{
|
|
98650
98834
|
name: "list_sessions",
|
|
@@ -98782,8 +98966,9 @@ var BUILDER_TOOL_NAMES = ["check"];
|
|
|
98782
98966
|
var READER_TOOLS = [
|
|
98783
98967
|
{
|
|
98784
98968
|
name: "update_centralu",
|
|
98969
|
+
// "apply" (#665) needs no words of its own here: 8 characters in every reader's request, and the install answer names it
|
|
98785
98970
|
description: "Updates Centralu with consent.",
|
|
98786
|
-
schema: external_exports.object({ action: external_exports.enum(["check", "install"]) }),
|
|
98971
|
+
schema: external_exports.object({ action: external_exports.enum(["check", "install", "apply"]) }),
|
|
98787
98972
|
// Updates are rare (#581); keep the definition behind Claude's tool search like app_guide.
|
|
98788
98973
|
deferred: true
|
|
98789
98974
|
},
|
|
@@ -98943,6 +99128,11 @@ function askProjectText(r) {
|
|
|
98943
99128
|
}
|
|
98944
99129
|
function agentRunText(r) {
|
|
98945
99130
|
if (!r.ok) return { text: r.error, isError: true };
|
|
99131
|
+
if (r.state === "pending") {
|
|
99132
|
+
return {
|
|
99133
|
+
text: `No session yet: starting ${r.tool} is waiting for the person's answer on the card in your session (request ${r.sessionId}). Carry on with other work, and do not do this with your own subagents instead. If they allow it, the session starts and you will be told when its turn ends; if they refuse or it is withdrawn, you will be told. agent_session(sessionId="${r.sessionId}", action="status") shows where it stands, action="wait" waits for the answer, action="stop" withdraws the request.`
|
|
99134
|
+
};
|
|
99135
|
+
}
|
|
98946
99136
|
const who = `the ${r.tool} session "${r.sessionName}" [${r.sessionId}]`;
|
|
98947
99137
|
switch (r.state) {
|
|
98948
99138
|
case "working":
|
|
@@ -98950,6 +99140,7 @@ function agentRunText(r) {
|
|
|
98950
99140
|
text: `${who} is working on it${r.notice ? ` \u2014 ${r.notice}` : ""}. ` + (r.reported ? "You will be told when it ends; " : "") + `agent_session(sessionId="${r.sessionId}", action="wait") waits for its answer, action="stop" stops it.`
|
|
98951
99141
|
};
|
|
98952
99142
|
case "stopped":
|
|
99143
|
+
if (!r.sessionName) return { text: `Withdrew request ${r.sessionId}: its card is gone and no session was started.` };
|
|
98953
99144
|
return { text: `Stopped ${who}.` };
|
|
98954
99145
|
case "deleted":
|
|
98955
99146
|
if (r.branch === void 0) return { text: `Moved ${who} to the trash. Tell the person in one line.` };
|
|
@@ -98975,13 +99166,25 @@ agent_session(sessionId="${r.sessionId}", action="send", text=\u2026) gives it m
|
|
|
98975
99166
|
async function runOrchestratorTool(tools, name, args, caller = { sessionId: null, profile: "human" }, signal) {
|
|
98976
99167
|
if (name === "update_centralu") {
|
|
98977
99168
|
const action = args.action;
|
|
98978
|
-
if (action !== "check" && action !== "install") return { text: "Give action: check or
|
|
98979
|
-
const r = tools.updateCentralu ? await tools.updateCentralu(action) : { ok: false, error: "Update checks are unavailable" };
|
|
99169
|
+
if (action !== "check" && action !== "install" && action !== "apply") return { text: "Give action: check, install or apply.", isError: true };
|
|
99170
|
+
const r = tools.updateCentralu ? await tools.updateCentralu(action, signal) : { ok: false, error: "Update checks are unavailable" };
|
|
99171
|
+
if (action === "apply") {
|
|
99172
|
+
if (!r.ok) return { text: `Centralu update was not applied: ${r.error ?? "unavailable"}`, isError: true };
|
|
99173
|
+
const v = r.relaunched?.version;
|
|
99174
|
+
return {
|
|
99175
|
+
text: `Centralu is relaunching${v ? ` into ${v}` : " into the installed build"}. Running agents, this one included, keep going; the new window then moves the agent host to the new build by itself, and a Centralu tool call still running then may be stopped and need trying again.`
|
|
99176
|
+
};
|
|
99177
|
+
}
|
|
98980
99178
|
if (!r.ok) return { text: `Centralu update did not start: ${r.error ?? "unavailable"}`, isError: true };
|
|
98981
99179
|
const s = r.status;
|
|
98982
99180
|
if (!s) return { text: "Centralu update is unavailable.", isError: true };
|
|
98983
99181
|
if (action === "check") return { text: s.newer && s.latest ? `Centralu ${s.latest} is available.` : s.error ?? "Centralu is up to date." };
|
|
98984
|
-
|
|
99182
|
+
const applies = "The person applies the installed build from Centralu, or update_centralu with action apply does; their automatic-update setting was not changed.";
|
|
99183
|
+
if (s.phase === "failed") return { text: `Centralu update failed: ${s.error}` };
|
|
99184
|
+
if (s.phase === "restart_required") return { text: `Centralu ${s.latest ?? ""} is installed. ${applies}` };
|
|
99185
|
+
if (s.phase === "updating" || s.phase === "waiting_for_npm") return { text: `Installing Centralu ${s.latest ?? ""}. ${applies}` };
|
|
99186
|
+
if (s.newer && s.latest) return { text: `Centralu ${s.latest} is available, but the install did not start.`, isError: true };
|
|
99187
|
+
return { text: s.error ?? "Centralu is up to date." };
|
|
98985
99188
|
}
|
|
98986
99189
|
if (name === "list_sessions") return listSessionsText(tools, caller);
|
|
98987
99190
|
if (name === "centralu_commands" || name === "centralu_help" || name === "centralu_run") return runCommandTool(tools, name, args, caller, signal);
|
|
@@ -101161,8 +101364,8 @@ function orchestratorMemory(reader, sessionId) {
|
|
|
101161
101364
|
|
|
101162
101365
|
// packages/agent-host/src/sessions/manager.ts
|
|
101163
101366
|
import { homedir as homedir12 } from "node:os";
|
|
101164
|
-
import { basename as basename5, isAbsolute as isAbsolute9, join as join29, relative as relative8, resolve as resolve11, sep as sep11 } from "node:path";
|
|
101165
|
-
import { existsSync as existsSync19, rmSync as rmSync7, statSync as statSync12 } from "node:fs";
|
|
101367
|
+
import { basename as basename5, dirname as dirname10, isAbsolute as isAbsolute9, join as join29, relative as relative8, resolve as resolve11, sep as sep11 } from "node:path";
|
|
101368
|
+
import { existsSync as existsSync19, realpathSync as realpathSync8, rmSync as rmSync7, statSync as statSync12 } from "node:fs";
|
|
101166
101369
|
import { exec as exec6 } from "node:child_process";
|
|
101167
101370
|
|
|
101168
101371
|
// packages/agent-host/src/adapters/claude/index.ts
|
|
@@ -102365,6 +102568,28 @@ var ClaudeBackgroundTracker = class {
|
|
|
102365
102568
|
recover(messages2) {
|
|
102366
102569
|
return this.schedules.recover(messages2) ? [this.event()] : [];
|
|
102367
102570
|
}
|
|
102571
|
+
/**
|
|
102572
|
+
* The tasks the previous host last saw running in an adopted process (#648, `ProcessSource.adopt.backgroundTasks`).
|
|
102573
|
+
* The CLI sends its level only when it changes, so without this a tracker over an adopted process would list nothing
|
|
102574
|
+
* and, since only a task the level once listed counts as background work, report none of their endings either: the
|
|
102575
|
+
* session would read as idle and be put to sleep, or restarted, over a running shell. The manager lists them itself;
|
|
102576
|
+
* this only makes the tracker know them, so the next `task_notification` or level is read against them.
|
|
102577
|
+
*/
|
|
102578
|
+
seed(tasks) {
|
|
102579
|
+
for (const t of tasks) {
|
|
102580
|
+
if (t.status !== "running" || t.stoppable === false || this.seen.has(t.id)) continue;
|
|
102581
|
+
this.info.set(t.id, {
|
|
102582
|
+
kind: t.kind,
|
|
102583
|
+
description: t.description,
|
|
102584
|
+
...t.parentCallId ? { parentCallId: t.parentCallId } : {},
|
|
102585
|
+
...t.ambient ? { ambient: true } : {},
|
|
102586
|
+
// `entry` derives stopsWithTurn from the kind unless the task is nested; a seeded one without it was nested
|
|
102587
|
+
...t.stopsWithTurn === void 0 && (t.kind === "agent" || t.kind === "shell") ? { nested: true } : {}
|
|
102588
|
+
});
|
|
102589
|
+
this.live.push(t.id);
|
|
102590
|
+
this.seen.add(t.id);
|
|
102591
|
+
}
|
|
102592
|
+
}
|
|
102368
102593
|
/** Drops the wake-ups whose time has passed; the new level when that changed anything */
|
|
102369
102594
|
tick() {
|
|
102370
102595
|
return this.schedules.prune() ? [this.event()] : [];
|
|
@@ -102392,7 +102617,7 @@ var ClaudeBackgroundTracker = class {
|
|
|
102392
102617
|
this.live.push(id2);
|
|
102393
102618
|
this.seen.add(id2);
|
|
102394
102619
|
}
|
|
102395
|
-
return [this.event()];
|
|
102620
|
+
return [this.event(void 0, true)];
|
|
102396
102621
|
}
|
|
102397
102622
|
if (subtype === "task_started" || subtype === "task_updated") {
|
|
102398
102623
|
const id2 = str4(m.task_id);
|
|
@@ -102458,9 +102683,10 @@ var ClaudeBackgroundTracker = class {
|
|
|
102458
102683
|
status: "running"
|
|
102459
102684
|
};
|
|
102460
102685
|
}
|
|
102461
|
-
event(ended) {
|
|
102686
|
+
event(ended, snapshot2) {
|
|
102462
102687
|
return {
|
|
102463
102688
|
type: "background_tasks",
|
|
102689
|
+
...snapshot2 ? { snapshot: snapshot2 } : {},
|
|
102464
102690
|
sessionId: this.sessionId,
|
|
102465
102691
|
live: [...this.live.map((id2) => this.entry(id2, this.info.get(id2))), ...this.schedules.live()],
|
|
102466
102692
|
...ended ? { ended } : {}
|
|
@@ -103047,6 +103273,7 @@ var ClaudeSession = class {
|
|
|
103047
103273
|
this.stream.goal.seed(opts.knownGoal);
|
|
103048
103274
|
if (opts.processSource?.adopt) this.externalId = opts.resumeExternalId ?? null;
|
|
103049
103275
|
if (opts.processSource?.adopt && opts.resumeExternalId) void this.recoverSchedules(opts.resumeExternalId, opts.cwd);
|
|
103276
|
+
if (opts.processSource?.adopt?.backgroundTasks?.length) this.stream.tasks.seed(opts.processSource.adopt.backgroundTasks);
|
|
103050
103277
|
}
|
|
103051
103278
|
sessionId;
|
|
103052
103279
|
opts;
|
|
@@ -106919,6 +107146,33 @@ function registeredTool(adapters2, preferred) {
|
|
|
106919
107146
|
return adapters2.keys().next().value ?? null;
|
|
106920
107147
|
}
|
|
106921
107148
|
|
|
107149
|
+
// packages/agent-host/src/sessions/tool-profiles.ts
|
|
107150
|
+
function isAppAgent(f) {
|
|
107151
|
+
return !!f.appId && f.kind === "worker" && !RESERVED_APP_IDS.includes(f.appId) && !f.builder;
|
|
107152
|
+
}
|
|
107153
|
+
function readsOwnProject(f) {
|
|
107154
|
+
return f.kind === "worker" && !!f.projectId && !isAppAgent(f) && f.sessionTools;
|
|
107155
|
+
}
|
|
107156
|
+
function toolProfileFor(f) {
|
|
107157
|
+
if (f.kind === "orchestrator") return "orchestrator";
|
|
107158
|
+
if (f.kind === "coordinator") return "scoped";
|
|
107159
|
+
if (f.builder) return "builder";
|
|
107160
|
+
return readsOwnProject(f) ? "reader" : null;
|
|
107161
|
+
}
|
|
107162
|
+
function toolBundleFor(profile, scopeSessionIds, sources) {
|
|
107163
|
+
switch (profile) {
|
|
107164
|
+
case "orchestrator":
|
|
107165
|
+
case "builder":
|
|
107166
|
+
return sources.orchestrator();
|
|
107167
|
+
case "scoped":
|
|
107168
|
+
return sources.scoped(scopeSessionIds ?? []);
|
|
107169
|
+
case "reader":
|
|
107170
|
+
return sources.reader();
|
|
107171
|
+
default:
|
|
107172
|
+
return void 0;
|
|
107173
|
+
}
|
|
107174
|
+
}
|
|
107175
|
+
|
|
106922
107176
|
// packages/agent-host/src/dev-services/store.ts
|
|
106923
107177
|
import Database from "better-sqlite3";
|
|
106924
107178
|
import { existsSync as existsSync18, readFileSync as readFileSync12 } from "node:fs";
|
|
@@ -108237,6 +108491,70 @@ var Store = class _Store {
|
|
|
108237
108491
|
WHERE asked_by_session_id IS NULL AND parent_session_id IS NOT NULL AND worktree_path IS NOT NULL
|
|
108238
108492
|
`);
|
|
108239
108493
|
}
|
|
108494
|
+
},
|
|
108495
|
+
{
|
|
108496
|
+
to: 49,
|
|
108497
|
+
breaksOlderReaders: false,
|
|
108498
|
+
/**
|
|
108499
|
+
* "You will be told when it ends" (#646) — `report_marks`, one row per session that owes a report to the session
|
|
108500
|
+
* that gave it work (`start_agent` with `wait: false` or past the bound, `send_to_session` with `reportBack`).
|
|
108501
|
+
*
|
|
108502
|
+
* session_id the session whose turn end is reported
|
|
108503
|
+
* caller_session_id the session told about it
|
|
108504
|
+
* marked_at when the report was asked for
|
|
108505
|
+
*
|
|
108506
|
+
* The mark used to live in the host's memory alone (`SessionManager.awaitingReport`), so a host swap (#280) lost
|
|
108507
|
+
* it: the keeper kept the child's process, it finished under the new host, and the parent was never woken. The
|
|
108508
|
+
* new host reads the rows at startup and keeps the marks of the sessions the keeper kept; a mark whose session
|
|
108509
|
+
* has no process any more is dropped, since the turn it was about died with the old host.
|
|
108510
|
+
*
|
|
108511
|
+
* Not foreign keys: a caller in the trash still has its mark cleared by the manager, and purging either session
|
|
108512
|
+
* deletes the row (`purgeSession`).
|
|
108513
|
+
*
|
|
108514
|
+
* **Expand only (#292's rule).** A new table; an older host never reads it.
|
|
108515
|
+
*/
|
|
108516
|
+
run: () => {
|
|
108517
|
+
this.db.exec(`
|
|
108518
|
+
CREATE TABLE IF NOT EXISTS report_marks (
|
|
108519
|
+
session_id TEXT PRIMARY KEY,
|
|
108520
|
+
caller_session_id TEXT NOT NULL,
|
|
108521
|
+
marked_at INTEGER NOT NULL
|
|
108522
|
+
)
|
|
108523
|
+
`);
|
|
108524
|
+
}
|
|
108525
|
+
},
|
|
108526
|
+
{
|
|
108527
|
+
to: 50,
|
|
108528
|
+
breaksOlderReaders: false,
|
|
108529
|
+
/**
|
|
108530
|
+
* start_agent requests still waiting for the person (#673) — `agent_start_requests`, one row per consent card
|
|
108531
|
+
* a session raised to start another agent, from the moment the card stands until the person answers it or it is
|
|
108532
|
+
* withdrawn.
|
|
108533
|
+
*
|
|
108534
|
+
* request_id the card's request id, which is also the handle the caller follows with agent_session
|
|
108535
|
+
* caller_session_id the session that asked
|
|
108536
|
+
* request what the start needs once allowed, as JSON (`AgentStartRequest` in the manager)
|
|
108537
|
+
* asked_at when the card was raised
|
|
108538
|
+
*
|
|
108539
|
+
* The card used to be withdrawn after 200 seconds, so an absent person never saw it. It now stands until it is
|
|
108540
|
+
* answered, which can outlive the host: a host swap (#280) or a restart reads these rows and raises the cards
|
|
108541
|
+
* again, and an approval starts the child then. A row whose caller is no longer there is dropped.
|
|
108542
|
+
*
|
|
108543
|
+
* Not foreign keys, as `report_marks`: trashing the caller withdraws its rows through the manager, and purging
|
|
108544
|
+
* deletes them (`purgeSession`).
|
|
108545
|
+
*
|
|
108546
|
+
* **Expand only (#292's rule).** A new table; an older host never reads it.
|
|
108547
|
+
*/
|
|
108548
|
+
run: () => {
|
|
108549
|
+
this.db.exec(`
|
|
108550
|
+
CREATE TABLE IF NOT EXISTS agent_start_requests (
|
|
108551
|
+
request_id TEXT PRIMARY KEY,
|
|
108552
|
+
caller_session_id TEXT NOT NULL,
|
|
108553
|
+
request TEXT NOT NULL,
|
|
108554
|
+
asked_at INTEGER NOT NULL
|
|
108555
|
+
)
|
|
108556
|
+
`);
|
|
108557
|
+
}
|
|
108240
108558
|
}
|
|
108241
108559
|
];
|
|
108242
108560
|
}
|
|
@@ -108430,11 +108748,16 @@ var Store = class _Store {
|
|
|
108430
108748
|
this.checkpoint();
|
|
108431
108749
|
this.db.close();
|
|
108432
108750
|
}
|
|
108751
|
+
/**
|
|
108752
|
+
* `trusted` is the trust a NEW row starts with (#655); absent means untrusted, as before. It is an insert-only
|
|
108753
|
+
* value: registering a path that already has a row renames it and leaves its trust alone, so neither an earlier
|
|
108754
|
+
* "no" nor a later "yes" is overwritten by a second registration.
|
|
108755
|
+
*/
|
|
108433
108756
|
addProject(p) {
|
|
108434
108757
|
this.db.prepare(
|
|
108435
|
-
`INSERT INTO projects (id, path, name, created_at) VALUES (?, ?, ?, ?)
|
|
108758
|
+
`INSERT INTO projects (id, path, name, created_at, trusted) VALUES (?, ?, ?, ?, ?)
|
|
108436
108759
|
ON CONFLICT(path) DO UPDATE SET name = excluded.name`
|
|
108437
|
-
).run(p.id, p.path, p.name, Date.now());
|
|
108760
|
+
).run(p.id, p.path, p.name, Date.now(), p.trusted ? 1 : 0);
|
|
108438
108761
|
}
|
|
108439
108762
|
/**
|
|
108440
108763
|
* `commands` is deliberately not in here (issue #44) — `projectCommands` answers that one.
|
|
@@ -109006,6 +109329,8 @@ var Store = class _Store {
|
|
|
109006
109329
|
this.db.prepare(`DELETE FROM commit_sessions WHERE session_id = ?`).run(sessionId);
|
|
109007
109330
|
this.db.prepare(`DELETE FROM app_run_failures WHERE run_id IN (SELECT id FROM app_runs WHERE session_id = ? OR caller_session_id = ?)`).run(sessionId, sessionId);
|
|
109008
109331
|
this.db.prepare(`DELETE FROM app_runs WHERE session_id = ? OR caller_session_id = ?`).run(sessionId, sessionId);
|
|
109332
|
+
this.db.prepare(`DELETE FROM report_marks WHERE session_id = ? OR caller_session_id = ?`).run(sessionId, sessionId);
|
|
109333
|
+
this.db.prepare(`DELETE FROM agent_start_requests WHERE caller_session_id = ?`).run(sessionId);
|
|
109009
109334
|
this.db.prepare(`DELETE FROM sessions WHERE id = ? AND deleted_at IS NOT NULL`).run(sessionId);
|
|
109010
109335
|
return true;
|
|
109011
109336
|
});
|
|
@@ -109593,6 +109918,39 @@ var Store = class _Store {
|
|
|
109593
109918
|
).all();
|
|
109594
109919
|
return rows.filter((r) => ProjectConsentKind.safeParse(r.kind).success);
|
|
109595
109920
|
}
|
|
109921
|
+
// ── Sessions that owe a report to the session that gave them work (#646) — see migration v49 ──
|
|
109922
|
+
listReportMarks() {
|
|
109923
|
+
return this.db.prepare(`SELECT session_id as sessionId, caller_session_id as callerSessionId FROM report_marks ORDER BY marked_at`).all();
|
|
109924
|
+
}
|
|
109925
|
+
setReportMark(sessionId, callerSessionId) {
|
|
109926
|
+
this.db.prepare(
|
|
109927
|
+
`INSERT INTO report_marks (session_id, caller_session_id, marked_at) VALUES (?, ?, ?)
|
|
109928
|
+
ON CONFLICT(session_id) DO UPDATE SET caller_session_id = excluded.caller_session_id, marked_at = excluded.marked_at`
|
|
109929
|
+
).run(sessionId, callerSessionId, Date.now());
|
|
109930
|
+
}
|
|
109931
|
+
clearReportMark(sessionId) {
|
|
109932
|
+
this.db.prepare(`DELETE FROM report_marks WHERE session_id = ?`).run(sessionId);
|
|
109933
|
+
}
|
|
109934
|
+
// ── start_agent requests waiting for the person (#673) — see migration v50 ──
|
|
109935
|
+
listAgentStartRequests() {
|
|
109936
|
+
const rows = this.db.prepare(`SELECT request_id as requestId, caller_session_id as callerSessionId, request, asked_at as askedAt FROM agent_start_requests ORDER BY asked_at`).all();
|
|
109937
|
+
return rows.flatMap((r) => {
|
|
109938
|
+
try {
|
|
109939
|
+
return [{ ...r, request: JSON.parse(r.request) }];
|
|
109940
|
+
} catch {
|
|
109941
|
+
return [];
|
|
109942
|
+
}
|
|
109943
|
+
});
|
|
109944
|
+
}
|
|
109945
|
+
putAgentStartRequest(requestId, callerSessionId, request) {
|
|
109946
|
+
this.db.prepare(
|
|
109947
|
+
`INSERT INTO agent_start_requests (request_id, caller_session_id, request, asked_at) VALUES (?, ?, ?, ?)
|
|
109948
|
+
ON CONFLICT(request_id) DO UPDATE SET caller_session_id = excluded.caller_session_id, request = excluded.request`
|
|
109949
|
+
).run(requestId, callerSessionId, JSON.stringify(request), Date.now());
|
|
109950
|
+
}
|
|
109951
|
+
deleteAgentStartRequest(requestId) {
|
|
109952
|
+
this.db.prepare(`DELETE FROM agent_start_requests WHERE request_id = ?`).run(requestId);
|
|
109953
|
+
}
|
|
109596
109954
|
// ── Linked machines (#82, v46) ─────────────────────────────────────────────────────────────
|
|
109597
109955
|
listLinkedMachines() {
|
|
109598
109956
|
const rows = this.db.prepare(
|
|
@@ -109762,9 +110120,9 @@ async function gitStatusFiles(cwd, trust = {}) {
|
|
|
109762
110120
|
if (entry.startsWith("1 ") || entry.startsWith("2 ")) {
|
|
109763
110121
|
const parts = entry.split(" ");
|
|
109764
110122
|
const xy = parts[1] ?? "..";
|
|
109765
|
-
const
|
|
109766
|
-
const path2 = parts.slice(
|
|
109767
|
-
if (
|
|
110123
|
+
const rename6 = entry.startsWith("2 ");
|
|
110124
|
+
const path2 = parts.slice(rename6 ? 9 : 8).join(" ");
|
|
110125
|
+
if (rename6) i++;
|
|
109768
110126
|
if (!path2) continue;
|
|
109769
110127
|
const [x, y] = [xy[0] ?? ".", xy[1] ?? "."];
|
|
109770
110128
|
if (x !== ".") out.push({ path: path2, staged: true, status: mapStatus(x) });
|
|
@@ -110448,12 +110806,72 @@ function attachCommitSessions(commits, records, nameOf) {
|
|
|
110448
110806
|
|
|
110449
110807
|
// packages/agent-host/src/sessions/manager.ts
|
|
110450
110808
|
function withTimeout(p, ms3, label = "A call") {
|
|
110809
|
+
let timer;
|
|
110451
110810
|
return Promise.race([
|
|
110452
110811
|
p,
|
|
110453
|
-
new Promise(
|
|
110454
|
-
|
|
110455
|
-
)
|
|
110456
|
-
]);
|
|
110812
|
+
new Promise((_, reject) => {
|
|
110813
|
+
timer = setTimeout(() => reject(new Error(`${label} did not finish within ${Math.round(ms3 / 1e3)}s`)), ms3);
|
|
110814
|
+
})
|
|
110815
|
+
]).finally(() => clearTimeout(timer));
|
|
110816
|
+
}
|
|
110817
|
+
var MODEL_LIST_READ_MS = 6e4;
|
|
110818
|
+
var MODEL_LIST_RETRY_MS = 6e4;
|
|
110819
|
+
var START_AGENT_MODEL_CHECK_MS = 3e3;
|
|
110820
|
+
var START_AGENT_SETUP_MS = 2e5;
|
|
110821
|
+
var START_AGENT_CALL_MS = 27e4;
|
|
110822
|
+
var ASK_THE_PERSON = "Do not do this with your own subagents instead: tell the person what stopped it and let them decide.";
|
|
110823
|
+
var AGENT_DEPTH_ERROR = "This session was itself started or asked by another session, so it cannot start agents \u2014 say in your answer what is needed instead.";
|
|
110824
|
+
function readAgentStartRequest(raw) {
|
|
110825
|
+
const r = raw;
|
|
110826
|
+
if (!r || typeof r !== "object") return null;
|
|
110827
|
+
if (typeof r.requestId !== "string" || typeof r.callerId !== "string" || typeof r.projectId !== "string") return null;
|
|
110828
|
+
if (typeof r.tool !== "string" || typeof r.task !== "string" || r.detail?.kind !== "agent_start") return null;
|
|
110829
|
+
return r;
|
|
110830
|
+
}
|
|
110831
|
+
function agentStartPending(req) {
|
|
110832
|
+
return { ok: true, state: "pending", sessionId: req.requestId, sessionName: "", tool: req.detail.tool };
|
|
110833
|
+
}
|
|
110834
|
+
function agentStartPersonText(req, how, why) {
|
|
110835
|
+
const what = `The request to start ${req.detail.tool} (\u201C${req.detail.text}\u201D)`;
|
|
110836
|
+
switch (how) {
|
|
110837
|
+
case "denied":
|
|
110838
|
+
return `${what} was denied. The session was told; no agent was started.`;
|
|
110839
|
+
case "cancelled":
|
|
110840
|
+
return `${what} was withdrawn: the call that asked was cancelled before you answered. No agent was started.`;
|
|
110841
|
+
case "deleted":
|
|
110842
|
+
return `${what} was withdrawn: this session was deleted. No agent was started.`;
|
|
110843
|
+
case "stopped":
|
|
110844
|
+
return `${what} was withdrawn by this session. No agent was started.`;
|
|
110845
|
+
case "untrusted":
|
|
110846
|
+
return `${what} was withdrawn: the project is no longer trusted. The session was told; no agent was started.`;
|
|
110847
|
+
case "failed":
|
|
110848
|
+
return `${what} was allowed, but no agent was started: ${(why ?? "").slice(0, 300)}`;
|
|
110849
|
+
default:
|
|
110850
|
+
return `${what} was withdrawn before you answered. No agent was started.`;
|
|
110851
|
+
}
|
|
110852
|
+
}
|
|
110853
|
+
function startAgentLate(progress) {
|
|
110854
|
+
const seconds = Math.round(START_AGENT_SETUP_MS / 1e3);
|
|
110855
|
+
const note = progress.unchecked ? ` (${progress.unchecked}.)` : "";
|
|
110856
|
+
switch (progress.stage) {
|
|
110857
|
+
case "created":
|
|
110858
|
+
return {
|
|
110859
|
+
ok: false,
|
|
110860
|
+
error: `The session [${progress.childId}] was created, but handing it the task took longer than ${seconds} seconds. Do not start another: agent_session(sessionId="${progress.childId}", action="status") shows where it stands. ${ASK_THE_PERSON}${note}`
|
|
110861
|
+
};
|
|
110862
|
+
case "creating":
|
|
110863
|
+
return {
|
|
110864
|
+
ok: false,
|
|
110865
|
+
error: `Starting the session took longer than ${seconds} seconds and had not finished, so a session may or may not exist. Do not call start_agent again until read_session (without a sessionId) shows whether a new session is there; if one starts, you will be told when it ends. ${ASK_THE_PERSON}${note}`
|
|
110866
|
+
};
|
|
110867
|
+
case "asking":
|
|
110868
|
+
return {
|
|
110869
|
+
ok: false,
|
|
110870
|
+
error: `No session was created: the person did not answer the question in this session within ${seconds} seconds, so it was withdrawn. ${ASK_THE_PERSON}`
|
|
110871
|
+
};
|
|
110872
|
+
default:
|
|
110873
|
+
return { ok: false, error: `No session was created: checking the request took longer than ${seconds} seconds. Try again once; if it repeats, ask the person. ${ASK_THE_PERSON}` };
|
|
110874
|
+
}
|
|
110457
110875
|
}
|
|
110458
110876
|
var HISTORY_LIMIT = 200;
|
|
110459
110877
|
var SYNC_LIMIT = 600;
|
|
@@ -110490,10 +110908,26 @@ function isCompactionMarker(row) {
|
|
|
110490
110908
|
return row.kind === "marker" && p?.type === "compaction" && !p.failed;
|
|
110491
110909
|
}
|
|
110492
110910
|
var inTurn = (state) => state === "working" || state === "waiting_approval";
|
|
110911
|
+
var BACKGROUND_UNKNOWN_MAX_MS = 2 * 60 * 60 * 1e3;
|
|
110912
|
+
var BACKGROUND_HOLD_MAX_MS = 60 * 60 * 1e3;
|
|
110913
|
+
function canonicalPath(path2) {
|
|
110914
|
+
const full = resolve11(path2);
|
|
110915
|
+
try {
|
|
110916
|
+
return realpathSync8.native(full);
|
|
110917
|
+
} catch {
|
|
110918
|
+
const parent = dirname10(full);
|
|
110919
|
+
return parent === full ? full : join29(canonicalPath(parent), basename5(full));
|
|
110920
|
+
}
|
|
110921
|
+
}
|
|
110922
|
+
function insideFolder(path2, folder) {
|
|
110923
|
+
const rel = relative8(folder, path2);
|
|
110924
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep11}`) && !isAbsolute9(rel);
|
|
110925
|
+
}
|
|
110493
110926
|
var EXTERNAL_LIST_LIMIT = 200;
|
|
110494
110927
|
var HOST_READ_MAX_LINES = 200;
|
|
110495
110928
|
var HOST_RECALL_MAX_HITS = 50;
|
|
110496
110929
|
var HUB_SENDER = "_hub-orchestrator";
|
|
110930
|
+
var HOST_SENDER = "_centralu";
|
|
110497
110931
|
var builderKey = (ref) => `${ref.projectId ?? "_user"}/${ref.appId}`;
|
|
110498
110932
|
var APP_BUILDERS_KEY = "apps.builders";
|
|
110499
110933
|
var SessionManager = class {
|
|
@@ -110526,8 +110960,29 @@ var SessionManager = class {
|
|
|
110526
110960
|
store2.upsertSession(fixed);
|
|
110527
110961
|
}
|
|
110528
110962
|
}
|
|
110963
|
+
for (const mark of store2.listReportMarks()) {
|
|
110964
|
+
const child = this.meta.get(mark.sessionId);
|
|
110965
|
+
const running = keeperOpts.keptSessions?.has(mark.sessionId) && !!child && inTurn(child.state) && this.meta.has(mark.callerSessionId);
|
|
110966
|
+
if (running) {
|
|
110967
|
+
this.awaitingReport.set(mark.sessionId, mark.callerSessionId);
|
|
110968
|
+
continue;
|
|
110969
|
+
}
|
|
110970
|
+
if (child && keeperOpts.keptSessions?.has(mark.sessionId)) {
|
|
110971
|
+
console.error(`[agent-host] report mark dropped: ${mark.sessionId.slice(0, 8)} ended its turn during the swap`);
|
|
110972
|
+
}
|
|
110973
|
+
store2.clearReportMark(mark.sessionId);
|
|
110974
|
+
}
|
|
110975
|
+
for (const row of store2.listAgentStartRequests()) {
|
|
110976
|
+
const req = readAgentStartRequest(row.request);
|
|
110977
|
+
if (!req || req.requestId !== row.requestId || req.callerId !== row.callerSessionId) {
|
|
110978
|
+
store2.deleteAgentStartRequest(row.requestId);
|
|
110979
|
+
continue;
|
|
110980
|
+
}
|
|
110981
|
+
this.resumeAgentStart(req);
|
|
110982
|
+
}
|
|
110529
110983
|
void this.sweepOrphanHandoffNotes().catch(() => {
|
|
110530
110984
|
});
|
|
110985
|
+
this.recoveryRepair = repairWorktreeRecoveryRemovals((id2) => store2.isTrashed(id2)).catch((e) => console.error(`[trash] recovery folders left by an interrupted purge were not repaired: ${e.message}`));
|
|
110531
110986
|
this.nameUnnamedWorktrees();
|
|
110532
110987
|
for (const pid of new Set(
|
|
110533
110988
|
[...this.meta.values()].filter((s) => s.worktree && s.projectId).map((s) => s.projectId)
|
|
@@ -110598,8 +111053,30 @@ var SessionManager = class {
|
|
|
110598
111053
|
cacheLedger = new CacheLedger();
|
|
110599
111054
|
/** Why a session's process is being restarted, for the cache's rebuild cause; taken when the new process comes up */
|
|
110600
111055
|
restartReasons = /* @__PURE__ */ new Map();
|
|
110601
|
-
/**
|
|
111056
|
+
/**
|
|
111057
|
+
* Session asked to be reported on completion -> orchestrator to notify (removed once notified). Mirrored in the
|
|
111058
|
+
* store's `report_marks` (v49, #646) through `markReport` / `clearReport`, so a host swap (#280) does not lose
|
|
111059
|
+
* "you will be told when it ends": the constructor reads the marks back for the sessions the keeper kept.
|
|
111060
|
+
*/
|
|
110602
111061
|
awaitingReport = /* @__PURE__ */ new Map();
|
|
111062
|
+
/**
|
|
111063
|
+
* `detachAll` is handing the sessions to the next host (#280): the waits it fails are not the end of their turns,
|
|
111064
|
+
* so no report goes out and the marks stay in the store for the host that sees those turns end (#646).
|
|
111065
|
+
*/
|
|
111066
|
+
handingOver = false;
|
|
111067
|
+
/**
|
|
111068
|
+
* An auto observer's own mid-turn note (#646), `<observer>><session>` -> the reader its receipt is held on. The
|
|
111069
|
+
* observer's `status` and `wait` read this, never the owner's delegation, until a read takes its settled outcome.
|
|
111070
|
+
*/
|
|
111071
|
+
observerNotes = /* @__PURE__ */ new Map();
|
|
111072
|
+
markReport(sessionId, callerId) {
|
|
111073
|
+
this.awaitingReport.set(sessionId, callerId);
|
|
111074
|
+
this.store.setReportMark(sessionId, callerId);
|
|
111075
|
+
}
|
|
111076
|
+
clearReport(sessionId) {
|
|
111077
|
+
if (!this.awaitingReport.delete(sessionId)) return;
|
|
111078
|
+
this.store.clearReportMark(sessionId);
|
|
111079
|
+
}
|
|
110603
111080
|
/**
|
|
110604
111081
|
* Session currently being resumed -> its promise.
|
|
110605
111082
|
*
|
|
@@ -110629,9 +111106,31 @@ var SessionManager = class {
|
|
|
110629
111106
|
definitionsRestartAfterTurn = /* @__PURE__ */ new Set();
|
|
110630
111107
|
/** A kept process was started with adapter settings from an older host build (#589). */
|
|
110631
111108
|
launchSettingsRestartAfterTurn = /* @__PURE__ */ new Set();
|
|
111109
|
+
/** Sessions whose pending launch-settings restart comes from a change of their project's trust (#663): the restart says so */
|
|
111110
|
+
trustRestartNotice = /* @__PURE__ */ new Set();
|
|
111111
|
+
/**
|
|
111112
|
+
* Sessions whose adopted process may hold background work this host knows nothing of (#648) -> when the session was
|
|
111113
|
+
* last heard from. The previous host left no list of it (a build before the list was kept), and the tool says its
|
|
111114
|
+
* level only when it changes. Until the tool's own full list arrives (`snapshot`), the session is never idle
|
|
111115
|
+
* (`sessionIdle`: `background_unknown`), so it is not put to sleep over a shell nobody can see; after
|
|
111116
|
+
* `BACKGROUND_UNKNOWN_MAX_MS` without a word it is taken as holding nothing (`backgroundKnown`). A safety restart does
|
|
111117
|
+
* not wait for it (`applyDeferredRestart`). A fresh process starts known: it holds nothing yet.
|
|
111118
|
+
*/
|
|
111119
|
+
backgroundUnknown = /* @__PURE__ */ new Map();
|
|
111120
|
+
/**
|
|
111121
|
+
* A deferred restart that changes nothing about safety, waiting for the session's background work to end (#648) ->
|
|
111122
|
+
* when the wait began, and the timer that applies it anyway `BACKGROUND_HOLD_MAX_MS` later. The entry outlives its
|
|
111123
|
+
* timer until the restart is applied: a timer that fires mid-turn finds the turn running, and the turn's end applies
|
|
111124
|
+
* the restart then, counting from `since` rather than starting a new hour.
|
|
111125
|
+
*/
|
|
111126
|
+
backgroundHolds = /* @__PURE__ */ new Map();
|
|
110632
111127
|
updates = null;
|
|
110633
111128
|
/** Sessions allowed to update once per host; an "always" answer adds this session only. */
|
|
110634
111129
|
updateConsents = /* @__PURE__ */ new Set();
|
|
111130
|
+
/** Asking a window to apply the installed update (#665); null on a host with no windows to ask (tests) */
|
|
111131
|
+
updateApply = null;
|
|
111132
|
+
/** Sessions whose "always" covers applying (#665), kept apart from check and install: a relaunch is the person's window */
|
|
111133
|
+
updateApplyConsents = /* @__PURE__ */ new Set();
|
|
110635
111134
|
/**
|
|
110636
111135
|
* Where external apps are attached to a session (M4 A-5). It is null on a host with no runtime
|
|
110637
111136
|
* (most tests), and in that case the session accepts no apps — it is optional, like any other
|
|
@@ -110681,6 +111180,21 @@ var SessionManager = class {
|
|
|
110681
111180
|
* `readGrants`: a restart asks again. The project-wide always is a stored consent instead.
|
|
110682
111181
|
*/
|
|
110683
111182
|
agentConsents = /* @__PURE__ */ new Set();
|
|
111183
|
+
/**
|
|
111184
|
+
* start_agent requests whose card stands or stood (#673) — request id -> the request. A card is no longer withdrawn
|
|
111185
|
+
* when the call's bound passes: the call answers "pending" with the request id as a handle, the card stays until the
|
|
111186
|
+
* person answers, and an approval starts the child then. Mirrored in the store's `agent_start_requests` while it
|
|
111187
|
+
* stands, so the next host raises it again (`resumeAgentStart`). Kept after it ends, for agent_session on the handle,
|
|
111188
|
+
* until the caller goes.
|
|
111189
|
+
*/
|
|
111190
|
+
agentStarts = /* @__PURE__ */ new Map();
|
|
111191
|
+
/**
|
|
111192
|
+
* Places taken by starts between their ceiling check and the child's delegation (#673 review) — project id -> how
|
|
111193
|
+
* many. Counted by `agentCeilingError` with the running children, so starts at once cannot pass the ceiling together.
|
|
111194
|
+
*/
|
|
111195
|
+
agentStartReserved = /* @__PURE__ */ new Map();
|
|
111196
|
+
/** `disposeAll` has begun (#673): a card it withdraws is not the person's answer, and its request stays stored */
|
|
111197
|
+
disposing = false;
|
|
110684
111198
|
/**
|
|
110685
111199
|
* What a session may read beyond its folder because another project handed it back (#371 part B) — session id ->
|
|
110686
111200
|
* resolved paths (files, or output folders). Live only: the grant lasts while this host serves the session, and a
|
|
@@ -110733,13 +111247,18 @@ var SessionManager = class {
|
|
|
110733
111247
|
{ code: "internal" }
|
|
110734
111248
|
);
|
|
110735
111249
|
}
|
|
110736
|
-
|
|
111250
|
+
/**
|
|
111251
|
+
* `trusted` is the trust a project new to this host starts with (#655); registering a folder that is already a
|
|
111252
|
+
* project never changes its trust. Registering an existing folder from the screen leaves it untrusted, as the
|
|
111253
|
+
* person decides; only `createProject` passes it, for a folder it made empty itself.
|
|
111254
|
+
*/
|
|
111255
|
+
async addProject(path2, opts = {}) {
|
|
110737
111256
|
if (!existsSync19(path2) || !statSync12(path2).isDirectory()) {
|
|
110738
111257
|
throw Object.assign(new Error(`Directory not found: ${path2}`), { code: "internal" });
|
|
110739
111258
|
}
|
|
110740
111259
|
const existing = this.store.findProjectByPath(path2);
|
|
110741
111260
|
const id2 = existing?.id ?? randomUUID10();
|
|
110742
|
-
this.store.addProject({ id: id2, path: path2, name: basename5(path2) });
|
|
111261
|
+
this.store.addProject({ id: id2, path: path2, name: basename5(path2), ...opts.trusted ? { trusted: true } : {} });
|
|
110743
111262
|
return this.projectInfo(id2, path2);
|
|
110744
111263
|
}
|
|
110745
111264
|
/**
|
|
@@ -110784,7 +111303,7 @@ var SessionManager = class {
|
|
|
110784
111303
|
/**
|
|
110785
111304
|
* Makes a new project (#515): a folder named `name` in `parent` (this machine's `~/Centralu` when absent), empty, or a
|
|
110786
111305
|
* clone of `source` made with this machine's git and saved sign-ins, then registers it. Never reuses anything
|
|
110787
|
-
* already there (`projectFolderTarget`). A clone that fails leaves nothing behind: the folder this made is removed
|
|
111306
|
+
* already there (`projectFolderTarget`). An empty folder is registered trusted, a clone untrusted (#655). A clone that fails leaves nothing behind: the folder this made is removed
|
|
110788
111307
|
* and no project is registered.
|
|
110789
111308
|
*/
|
|
110790
111309
|
async createProject(name, parent, source) {
|
|
@@ -110801,7 +111320,7 @@ var SessionManager = class {
|
|
|
110801
111320
|
}
|
|
110802
111321
|
}
|
|
110803
111322
|
console.error(`[projects] made ${target.path}`);
|
|
110804
|
-
const info = await this.addProject(target.path);
|
|
111323
|
+
const info = await this.addProject(target.path, { trusted: !source });
|
|
110805
111324
|
this.emit({ type: "projects_changed" });
|
|
110806
111325
|
return info;
|
|
110807
111326
|
}
|
|
@@ -110895,13 +111414,29 @@ var SessionManager = class {
|
|
|
110895
111414
|
}
|
|
110896
111415
|
/**
|
|
110897
111416
|
* Project trust (M4 A-2, plan decision 3). The app runtime reads it fresh from the store every
|
|
110898
|
-
* time, so
|
|
110899
|
-
*
|
|
111417
|
+
* time, so telling it to rescan is the RPC layer's job (this layer does not know about the runtime).
|
|
111418
|
+
*
|
|
111419
|
+
* Trust decides which setting files a session reads (Claude `settingSources`, Codex `repoFilesConfig`), and a
|
|
111420
|
+
* live process keeps the ones it started with (#663). The flag is part of the launch-settings fingerprint, so a
|
|
111421
|
+
* change goes through the same safety restart as any other launch-settings change (#656): an idle session of the
|
|
111422
|
+
* project restarts now, one in a turn at the end of the turn, background work or not, and the conversation says
|
|
111423
|
+
* why. The flag is written before this returns; the promise settles when the idle restarts are done.
|
|
110900
111424
|
*/
|
|
110901
111425
|
setProjectTrusted(projectId, trusted) {
|
|
110902
111426
|
if (!this.store.setProjectTrusted(projectId, trusted)) {
|
|
110903
111427
|
throw Object.assign(new Error(`Project not found: ${projectId}`), { code: "internal" });
|
|
110904
111428
|
}
|
|
111429
|
+
if (!trusted) {
|
|
111430
|
+
for (const rec of [...this.agentStarts.values()]) {
|
|
111431
|
+
if (rec.ended || rec.req.projectId !== projectId) continue;
|
|
111432
|
+
this.withdrawAgentStart(
|
|
111433
|
+
rec.req.requestId,
|
|
111434
|
+
`The person stopped trusting this session's project, so no agent was started. ${ASK_THE_PERSON}`,
|
|
111435
|
+
agentStartPersonText(rec.req, "untrusted")
|
|
111436
|
+
);
|
|
111437
|
+
}
|
|
111438
|
+
}
|
|
111439
|
+
return this.refreshLaunchSettings(projectId, true);
|
|
110905
111440
|
}
|
|
110906
111441
|
/**
|
|
110907
111442
|
* Is this project trusted (decision 3, #92) — passed to the adapter when launching a session. False
|
|
@@ -111351,6 +111886,7 @@ var SessionManager = class {
|
|
|
111351
111886
|
let handle;
|
|
111352
111887
|
if (params.builderOf) this.setBuilder(params.builderOf, id2);
|
|
111353
111888
|
const apps = this.appsFor(info);
|
|
111889
|
+
const profile = toolProfileFor(this.profileFacts(info, !!params.builderOf));
|
|
111354
111890
|
const from = this.handleSink();
|
|
111355
111891
|
this.unborn.set(id2, []);
|
|
111356
111892
|
try {
|
|
@@ -111374,14 +111910,14 @@ var SessionManager = class {
|
|
|
111374
111910
|
...this.handoffReadDirs(params.projectId, !!params.handoff?.fromSessionId),
|
|
111375
111911
|
// Paths another project handed back to this session (#371), asked on every read
|
|
111376
111912
|
mayRead: (path2) => this.mayRead(id2, path2, cwd),
|
|
111377
|
-
// An orchestrator gets all of them
|
|
111378
|
-
//
|
|
111379
|
-
orchestratorTools:
|
|
111380
|
-
toolProfile:
|
|
111913
|
+
// An orchestrator gets all of them, a builder session (M4 C-3) just its own app's check (the bundle blocks
|
|
111914
|
+
// the rest), an ordinary session its own project, read-only (#320, #561)
|
|
111915
|
+
orchestratorTools: this.toolBundleOf(id2, profile, info),
|
|
111916
|
+
toolProfile: profile ?? void 0,
|
|
111381
111917
|
systemPromptAppend: info.kind === "orchestrator" ? orchestratorRole() + this.skillsPrompt() : info.roleAppend ?? void 0,
|
|
111382
111918
|
// The path back to the host — used by the orchestrator tools' bridge and an external app's bridge
|
|
111383
111919
|
// (M4 A-5)
|
|
111384
|
-
orchestratorBridge:
|
|
111920
|
+
orchestratorBridge: profile || apps ? this.endpoint?.(id2) ?? void 0 : void 0,
|
|
111385
111921
|
// An MCP server the person approved is loaded here as a user-folder app (M4 A-7, decision 4)
|
|
111386
111922
|
apps,
|
|
111387
111923
|
// The answer schema an app supplied when it asked for this (M4 D-1) — Claude gets it on this
|
|
@@ -111417,6 +111953,7 @@ var SessionManager = class {
|
|
|
111417
111953
|
}
|
|
111418
111954
|
}
|
|
111419
111955
|
this.handles.set(id2, handle);
|
|
111956
|
+
this.rememberBackgroundTasks(info);
|
|
111420
111957
|
this.cacheLedger.note(id2, params.resumeExternalId ? "new_process" : "cold_start");
|
|
111421
111958
|
this.running.set(id2, {
|
|
111422
111959
|
model: info.model,
|
|
@@ -111621,16 +112158,86 @@ var SessionManager = class {
|
|
|
111621
112158
|
* The keeper as a `ProcessSource` for one session, when this host runs under one (#280 step 2). `version` is the CLI
|
|
111622
112159
|
* version a process spawned now is started from, written on its keeper tag (#297).
|
|
111623
112160
|
*/
|
|
111624
|
-
processSourceFor(sessionId, tool, adopt, version2) {
|
|
112161
|
+
processSourceFor(sessionId, tool, adopt, version2, kept) {
|
|
111625
112162
|
const host = this.keeperOpts.processes;
|
|
111626
112163
|
if (!host) return {};
|
|
111627
112164
|
return {
|
|
111628
112165
|
processSource: {
|
|
111629
112166
|
spawn: (spec) => host.spawn(sessionId, tool, spec, version2 ?? void 0),
|
|
111630
|
-
...adopt ? { adopt: { process: adopt, openCalls: this.openToolCalls(sessionId) } } : {}
|
|
112167
|
+
...adopt ? { adopt: { process: adopt, openCalls: this.openToolCalls(sessionId), backgroundTasks: kept ?? [] } } : {}
|
|
111631
112168
|
}
|
|
111632
112169
|
};
|
|
111633
112170
|
}
|
|
112171
|
+
/**
|
|
112172
|
+
* The running background tasks of a session's process, kept in the store (#648) so the host that adopts the process
|
|
112173
|
+
* after a swap knows what it holds: the tool lists its level only when it changes, and `close()` on a process kills
|
|
112174
|
+
* its shells silently. Written on every change and when a process starts (empty). `null` when no host ever wrote it
|
|
112175
|
+
* for this session, which an adopted process reads as "unknown".
|
|
112176
|
+
*/
|
|
112177
|
+
backgroundTasksKey(sessionId) {
|
|
112178
|
+
return `background_tasks:${sessionId}`;
|
|
112179
|
+
}
|
|
112180
|
+
keptBackgroundTasks(sessionId) {
|
|
112181
|
+
const raw = this.store.appSetting(this.backgroundTasksKey(sessionId));
|
|
112182
|
+
if (raw === null) return null;
|
|
112183
|
+
try {
|
|
112184
|
+
const parsed = JSON.parse(raw);
|
|
112185
|
+
return Array.isArray(parsed) ? parsed.filter((t) => t && typeof t.id === "string" && t.status === "running") : [];
|
|
112186
|
+
} catch {
|
|
112187
|
+
return [];
|
|
112188
|
+
}
|
|
112189
|
+
}
|
|
112190
|
+
rememberBackgroundTasks(m) {
|
|
112191
|
+
this.store.setAppSetting(this.backgroundTasksKey(m.id), JSON.stringify(m.backgroundTasks.filter((t) => t.status === "running")));
|
|
112192
|
+
}
|
|
112193
|
+
/** Background work the session's process holds that ending it would end (#648): what its tool listed as running */
|
|
112194
|
+
backgroundRunning(sessionId) {
|
|
112195
|
+
const m = this.meta.get(sessionId);
|
|
112196
|
+
return !!m && liveBackgroundTasks(m.backgroundTasks).length > 0;
|
|
112197
|
+
}
|
|
112198
|
+
/**
|
|
112199
|
+
* Whether what the session's process holds in the background is known (#648). An adopted process with no record is
|
|
112200
|
+
* not, until its tool lists its work; after `BACKGROUND_UNKNOWN_MAX_MS` without a word from the session it is taken
|
|
112201
|
+
* as holding nothing, said in the log, and recorded so the next host does not start from unknown again.
|
|
112202
|
+
*/
|
|
112203
|
+
backgroundKnown(sessionId) {
|
|
112204
|
+
const since = this.backgroundUnknown.get(sessionId);
|
|
112205
|
+
if (since === void 0) return true;
|
|
112206
|
+
if (Date.now() - since < BACKGROUND_UNKNOWN_MAX_MS) return false;
|
|
112207
|
+
this.backgroundUnknown.delete(sessionId);
|
|
112208
|
+
console.error(
|
|
112209
|
+
`[sessions] ${sessionId.slice(0, 8)}: no word of its background work since it was adopted, for ${Math.round(BACKGROUND_UNKNOWN_MAX_MS / 36e5)} h; taking it as none`
|
|
112210
|
+
);
|
|
112211
|
+
const m = this.meta.get(sessionId);
|
|
112212
|
+
if (m) this.rememberBackgroundTasks(m);
|
|
112213
|
+
return true;
|
|
112214
|
+
}
|
|
112215
|
+
/** The session's process is gone for good (deleted): nothing of its background work is kept (#648) */
|
|
112216
|
+
forgetBackground(sessionId) {
|
|
112217
|
+
this.backgroundUnknown.delete(sessionId);
|
|
112218
|
+
this.clearBackgroundHold(sessionId);
|
|
112219
|
+
this.store.deleteAppSetting(this.backgroundTasksKey(sessionId));
|
|
112220
|
+
}
|
|
112221
|
+
/**
|
|
112222
|
+
* Says in the conversation that a restart is about to end the session's background work (#648): the tool's own
|
|
112223
|
+
* ending of each task follows in the list, but a person whose dev server stopped should read why where they look.
|
|
112224
|
+
*/
|
|
112225
|
+
noticeBackgroundStopped(sessionId, cause2) {
|
|
112226
|
+
const m = this.meta.get(sessionId);
|
|
112227
|
+
if (!m || !this.handles.has(sessionId)) return;
|
|
112228
|
+
const why = cause2 === "launch_settings" ? "changed launch settings" : cause2 === "instructions" ? "changed Centralu tool definitions" : cause2 === "thread_restart" ? "a change of attached apps" : cause2 === "model_change" ? "a model change" : this.running.get(sessionId)?.permissionPreset !== m.permissionPreset ? "a permission change" : "changed settings";
|
|
112229
|
+
const tasks = liveBackgroundTasks(m.backgroundTasks);
|
|
112230
|
+
const text2 = tasks.length > 0 ? `Restarting the agent to apply ${why}. The background work it was running stops with it: ${tasks.map((t) => t.description).join(", ")}. Ask the agent to start it again if you still need it.` : this.backgroundUnknown.has(sessionId) ? `Restarting the agent to apply ${why}. Any background work it started before Centralu was updated (a dev server, a watcher) stops with it; ask the agent to start it again if you still need it.` : null;
|
|
112231
|
+
if (text2) this.onEvent({ type: "notice", sessionId, level: "warning", from: "Centralu", label: "restart", text: text2 });
|
|
112232
|
+
}
|
|
112233
|
+
/** Says in the conversation that the restart in progress applies a change of the project's trust (#663) */
|
|
112234
|
+
noticeTrustRestart(sessionId) {
|
|
112235
|
+
if (!this.trustRestartNotice.delete(sessionId)) return;
|
|
112236
|
+
const m = this.meta.get(sessionId);
|
|
112237
|
+
if (!m || !this.handles.has(sessionId)) return;
|
|
112238
|
+
const text2 = this.projectTrusted(m.projectId) ? "Restarted the agent to apply this project's new trust: it now reads the project's own settings and instruction files (CLAUDE.md, AGENTS.md)." : "Restarted the agent to apply this project's removed trust: it now reads only your own settings, not the project's files.";
|
|
112239
|
+
this.onEvent({ type: "notice", sessionId, level: "info", from: "Centralu", label: "restart", text: text2 });
|
|
112240
|
+
}
|
|
111634
112241
|
/** Tool calls the store holds for this session with no result after them (the newest 200 rows) */
|
|
111635
112242
|
openToolCalls(sessionId) {
|
|
111636
112243
|
const open6 = /* @__PURE__ */ new Map();
|
|
@@ -111673,7 +112280,7 @@ var SessionManager = class {
|
|
|
111673
112280
|
);
|
|
111674
112281
|
}
|
|
111675
112282
|
async doResumeSession(sessionId, adopt) {
|
|
111676
|
-
if (this.
|
|
112283
|
+
if (this.worktreeCleanupBlocks(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
111677
112284
|
const m = this.meta.get(sessionId);
|
|
111678
112285
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
111679
112286
|
if (this.handles.has(sessionId)) return { session: m, resumed: true };
|
|
@@ -111715,6 +112322,7 @@ var SessionManager = class {
|
|
|
111715
112322
|
}
|
|
111716
112323
|
const apps = this.appsFor(m);
|
|
111717
112324
|
const builds = this.builderRefOf(m) !== null;
|
|
112325
|
+
const profile = toolProfileFor(this.profileFacts(m, builds));
|
|
111718
112326
|
const from = this.handleSink();
|
|
111719
112327
|
const launched = {
|
|
111720
112328
|
model: m.model,
|
|
@@ -111724,12 +112332,13 @@ var SessionManager = class {
|
|
|
111724
112332
|
permissionPreset: m.permissionPreset
|
|
111725
112333
|
};
|
|
111726
112334
|
if (!adopt) m.agentVersion = this.versionHint?.(tool) ?? null;
|
|
112335
|
+
const kept = adopt ? this.keptBackgroundTasks(sessionId) : null;
|
|
111727
112336
|
try {
|
|
111728
112337
|
const creating = adapter.createSession(
|
|
111729
112338
|
{
|
|
111730
112339
|
sessionId,
|
|
111731
112340
|
cwd,
|
|
111732
|
-
...this.processSourceFor(sessionId, tool, adopt, m.agentVersion),
|
|
112341
|
+
...this.processSourceFor(sessionId, tool, adopt, m.agentVersion, kept),
|
|
111733
112342
|
model: launched.model ?? void 0,
|
|
111734
112343
|
effort: launched.effort ?? void 0,
|
|
111735
112344
|
verbosity: launched.verbosity ?? void 0,
|
|
@@ -111761,11 +112370,8 @@ var SessionManager = class {
|
|
|
111761
112370
|
* has just been attached passes this check the next time it wakes and receives the
|
|
111762
112371
|
* child-only tools and role.
|
|
111763
112372
|
*/
|
|
111764
|
-
orchestratorTools:
|
|
111765
|
-
|
|
111766
|
-
this.readsOwnProject(m) ? this.readerToolsFor(sessionId) : void 0
|
|
111767
|
-
),
|
|
111768
|
-
toolProfile: m.kind === "orchestrator" ? "orchestrator" : m.kind === "coordinator" ? "scoped" : builds ? "builder" : this.readsOwnProject(m) ? "reader" : void 0,
|
|
112373
|
+
orchestratorTools: this.toolBundleOf(sessionId, profile, m),
|
|
112374
|
+
toolProfile: profile ?? void 0,
|
|
111769
112375
|
/*
|
|
111770
112376
|
* This is **where memory is handed over.** Switching tools breaks externalId, which routes
|
|
111771
112377
|
* things down this path (a new process), and that is when the past conversation rides along
|
|
@@ -111781,7 +112387,7 @@ var SessionManager = class {
|
|
|
111781
112387
|
// for a builder session (C-2) even if the app is broken
|
|
111782
112388
|
m.roleAppend ?? void 0
|
|
111783
112389
|
),
|
|
111784
|
-
orchestratorBridge:
|
|
112390
|
+
orchestratorBridge: profile || apps ? this.endpoint?.(sessionId) ?? void 0 : void 0,
|
|
111785
112391
|
// An approved MCP server is a user-folder app — an orchestrator receives those apps right here (M4
|
|
111786
112392
|
// A-7)
|
|
111787
112393
|
apps
|
|
@@ -111807,6 +112413,8 @@ var SessionManager = class {
|
|
|
111807
112413
|
if (!adopt) {
|
|
111808
112414
|
this.rememberToolPrefix(sessionId);
|
|
111809
112415
|
this.rememberLaunchSettings(m, cwd);
|
|
112416
|
+
this.backgroundUnknown.delete(sessionId);
|
|
112417
|
+
this.rememberBackgroundTasks(m);
|
|
111810
112418
|
}
|
|
111811
112419
|
this.emit({ type: "session_live", sessionId, live: true });
|
|
111812
112420
|
this.cacheLedger.note(sessionId, this.restartReasons.get(sessionId) ?? "new_process");
|
|
@@ -111817,6 +112425,8 @@ var SessionManager = class {
|
|
|
111817
112425
|
if (adopt) {
|
|
111818
112426
|
this.store.upsertSession(m);
|
|
111819
112427
|
this.emit({ type: "state_change", sessionId, state: m.state, reason: "reattached" });
|
|
112428
|
+
if (kept === null) this.backgroundUnknown.set(sessionId, Date.now());
|
|
112429
|
+
else if (kept.length > 0) this.onEvent({ type: "background_tasks", sessionId, live: kept });
|
|
111820
112430
|
} else {
|
|
111821
112431
|
m.state = "idle";
|
|
111822
112432
|
m.waitingSince = null;
|
|
@@ -111862,7 +112472,7 @@ var SessionManager = class {
|
|
|
111862
112472
|
* exactly as it was, and only what happens from here on piles up in the copy.
|
|
111863
112473
|
*/
|
|
111864
112474
|
async forkConversation(sessionId) {
|
|
111865
|
-
if (this.
|
|
112475
|
+
if (this.worktreeCleanupBlocks(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
111866
112476
|
const m = this.meta.get(sessionId);
|
|
111867
112477
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
111868
112478
|
const source = m.externalId ?? m.importedFrom;
|
|
@@ -111916,7 +112526,10 @@ var SessionManager = class {
|
|
|
111916
112526
|
*/
|
|
111917
112527
|
async trashSession(sessionId, removeWorktree = false, removeExternal = false, recovery) {
|
|
111918
112528
|
const pending = this.trashing.get(sessionId);
|
|
111919
|
-
if (pending)
|
|
112529
|
+
if (pending) {
|
|
112530
|
+
if (recovery) throw Object.assign(new Error("The session is already being deleted without its recovery snapshot"), { code: "internal" });
|
|
112531
|
+
return pending;
|
|
112532
|
+
}
|
|
111920
112533
|
const deleting = this.trashSessionOnce(sessionId, removeWorktree, removeExternal, recovery);
|
|
111921
112534
|
this.trashing.set(sessionId, deleting);
|
|
111922
112535
|
try {
|
|
@@ -111950,6 +112563,13 @@ var SessionManager = class {
|
|
|
111950
112563
|
}
|
|
111951
112564
|
this.readGrants.delete(sessionId);
|
|
111952
112565
|
this.agentConsents.delete(sessionId);
|
|
112566
|
+
for (const rec of [...this.agentStarts.values()]) {
|
|
112567
|
+
if (rec.req.callerId !== sessionId) continue;
|
|
112568
|
+
this.withdrawAgentStart(rec.req.requestId, "The session that asked was deleted.", agentStartPersonText(rec.req, "deleted"));
|
|
112569
|
+
this.agentStarts.delete(rec.req.requestId);
|
|
112570
|
+
}
|
|
112571
|
+
this.updateConsents.delete(sessionId);
|
|
112572
|
+
this.updateApplyConsents.delete(sessionId);
|
|
111953
112573
|
for (const ask of [...this.hostAsks.values()]) {
|
|
111954
112574
|
if (ask.sessionId !== sessionId) continue;
|
|
111955
112575
|
this.hostAsks.delete(ask.requestId);
|
|
@@ -111961,14 +112581,18 @@ var SessionManager = class {
|
|
|
111961
112581
|
});
|
|
111962
112582
|
this.handles.delete(sessionId);
|
|
111963
112583
|
}
|
|
112584
|
+
this.forgetBackground(sessionId);
|
|
111964
112585
|
this.closeStream(sessionId);
|
|
111965
112586
|
let project2;
|
|
111966
112587
|
if (m.projectId) project2 = this.store.listProjects().find((p) => p.id === m.projectId);
|
|
111967
112588
|
this.running.delete(sessionId);
|
|
111968
112589
|
this.restartAfterTurn.delete(sessionId);
|
|
112590
|
+
this.trustRestartNotice.delete(sessionId);
|
|
111969
112591
|
this.prCheckedAt.delete(sessionId);
|
|
111970
112592
|
this.appsRestartAfterTurn.delete(sessionId);
|
|
111971
|
-
this.
|
|
112593
|
+
this.clearReport(sessionId);
|
|
112594
|
+
for (const [child, caller] of [...this.awaitingReport]) if (caller === sessionId) this.clearReport(child);
|
|
112595
|
+
for (const key of [...this.observerNotes.keys()]) if (key.startsWith(`${sessionId}>`) || key.endsWith(`>${sessionId}`)) this.observerNotes.delete(key);
|
|
111972
112596
|
this.forgetCommits(sessionId);
|
|
111973
112597
|
this.meta.delete(sessionId);
|
|
111974
112598
|
await this.store.trashSession(sessionId, {
|
|
@@ -111982,6 +112606,8 @@ var SessionManager = class {
|
|
|
111982
112606
|
this.appsHub?.sessionGone(sessionId);
|
|
111983
112607
|
this.emit({ type: "session_deleted", sessionId });
|
|
111984
112608
|
}
|
|
112609
|
+
/** The startup repair of recovery folders an interrupted purge left aside (#647) */
|
|
112610
|
+
recoveryRepair = Promise.resolve();
|
|
111985
112611
|
/** Only one trash operation per session at a time — a purge racing a restore would restore half of a deleted session */
|
|
111986
112612
|
trashBusy = /* @__PURE__ */ new Set();
|
|
111987
112613
|
async trashOp(sessionId, run) {
|
|
@@ -112046,6 +112672,7 @@ var SessionManager = class {
|
|
|
112046
112672
|
*/
|
|
112047
112673
|
async restoreSession(sessionId) {
|
|
112048
112674
|
return this.trashOp(sessionId, async () => {
|
|
112675
|
+
await this.recoveryRepair;
|
|
112049
112676
|
const t = this.store.trashedSession(sessionId);
|
|
112050
112677
|
if (!t) throw Object.assign(new Error(`Not in the trash: ${sessionId}`), { code: "session_not_found" });
|
|
112051
112678
|
const { record: record2 } = t;
|
|
@@ -112102,10 +112729,12 @@ var SessionManager = class {
|
|
|
112102
112729
|
* believes 550MB is gone). It is skipped when a live session holds the same conversation (it was pulled back
|
|
112103
112730
|
* from Past conversations meanwhile): deleting it would take that session's history. Then the worktree, when
|
|
112104
112731
|
* chosen (a failure does not stop the purge — its folder is plain files the person can still remove). Then our
|
|
112105
|
-
* rows, and last the attachments and the handoff note, which have to outlive the rows that
|
|
112732
|
+
* rows, and last the recovery files, the attachments and the handoff note, which have to outlive the rows that
|
|
112733
|
+
* point at them.
|
|
112106
112734
|
*/
|
|
112107
112735
|
async purgeSession(sessionId) {
|
|
112108
112736
|
return this.trashOp(sessionId, async () => {
|
|
112737
|
+
await this.recoveryRepair;
|
|
112109
112738
|
const t = this.store.trashedSession(sessionId);
|
|
112110
112739
|
if (!t) throw Object.assign(new Error(`Not in the trash: ${sessionId}`), { code: "session_not_found" });
|
|
112111
112740
|
const { session: s, record: record2 } = t;
|
|
@@ -112128,8 +112757,16 @@ var SessionManager = class {
|
|
|
112128
112757
|
await gitWorktreeRemove(record2.projectPath, s.worktree.path, true).catch(() => {
|
|
112129
112758
|
});
|
|
112130
112759
|
}
|
|
112131
|
-
await
|
|
112132
|
-
|
|
112760
|
+
const recoveryRemoval = await stageWorktreeRecoveryRemoval(sessionId);
|
|
112761
|
+
try {
|
|
112762
|
+
await this.store.purgeSession(sessionId);
|
|
112763
|
+
this.forgetBackground(sessionId);
|
|
112764
|
+
} catch (e) {
|
|
112765
|
+
await recoveryRemoval.abort().catch((cause2) => console.error(`[trash] recovery files of ${sessionId.slice(0, 8)} could not be put back after a failed purge and stay aside until the next start: ${cause2.message}`));
|
|
112766
|
+
throw e;
|
|
112767
|
+
}
|
|
112768
|
+
await recoveryRemoval.commit().catch(() => {
|
|
112769
|
+
});
|
|
112133
112770
|
await clearAttachments(sessionId).catch(() => {
|
|
112134
112771
|
});
|
|
112135
112772
|
if (record2.projectId) await this.sweepOrphanHandoffNotes(record2.projectId).catch(() => {
|
|
@@ -112200,8 +112837,8 @@ var SessionManager = class {
|
|
|
112200
112837
|
const m = this.meta.get(sessionId);
|
|
112201
112838
|
return live && m && live.model !== m.model ? "model_change" : void 0;
|
|
112202
112839
|
}
|
|
112203
|
-
async updateSettings(sessionId, s) {
|
|
112204
|
-
if (this.
|
|
112840
|
+
async updateSettings(sessionId, s, by = "person") {
|
|
112841
|
+
if (this.worktreeCleanupBlocks(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
112205
112842
|
const m = this.meta.get(sessionId);
|
|
112206
112843
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
112207
112844
|
if (s.model !== void 0 && s.model !== m.model) this.cacheLedger.note(sessionId, "model_change");
|
|
@@ -112214,6 +112851,16 @@ var SessionManager = class {
|
|
|
112214
112851
|
if ((s.model !== void 0 || s.effort !== void 0) && m.projectId) {
|
|
112215
112852
|
this.store.setProjectToolDefaults(m.projectId, m.tool, { model: m.model, effort: m.effort });
|
|
112216
112853
|
}
|
|
112854
|
+
this.emit({
|
|
112855
|
+
type: "settings_changed",
|
|
112856
|
+
sessionId,
|
|
112857
|
+
model: m.model,
|
|
112858
|
+
effort: m.effort,
|
|
112859
|
+
verbosity: m.verbosity,
|
|
112860
|
+
serviceTier: m.serviceTier,
|
|
112861
|
+
permissionPreset: m.permissionPreset,
|
|
112862
|
+
...by === "person" ? { person: true } : { by }
|
|
112863
|
+
});
|
|
112217
112864
|
await this.resuming.get(sessionId)?.catch(() => {
|
|
112218
112865
|
});
|
|
112219
112866
|
const handle = this.handles.get(sessionId);
|
|
@@ -112296,6 +112943,7 @@ var SessionManager = class {
|
|
|
112296
112943
|
}
|
|
112297
112944
|
const wasBusy = inTurn(m.state);
|
|
112298
112945
|
this.applyStateHint(e, m);
|
|
112946
|
+
if (this.backgroundUnknown.has(m.id)) this.backgroundUnknown.set(m.id, Date.now());
|
|
112299
112947
|
if (wasBusy && !inTurn(m.state)) {
|
|
112300
112948
|
endedTurn = true;
|
|
112301
112949
|
const ref = this.builderRefOf(m);
|
|
@@ -112313,6 +112961,7 @@ var SessionManager = class {
|
|
|
112313
112961
|
});
|
|
112314
112962
|
this.handles.delete(e.sessionId);
|
|
112315
112963
|
this.running.delete(e.sessionId);
|
|
112964
|
+
this.emit({ type: "session_live", sessionId: e.sessionId, live: false, reason: "crashed" });
|
|
112316
112965
|
}
|
|
112317
112966
|
}
|
|
112318
112967
|
}
|
|
@@ -112322,13 +112971,37 @@ var SessionManager = class {
|
|
|
112322
112971
|
this.emit(seq != null ? { ...e, seq } : e);
|
|
112323
112972
|
if (endedTurn && e.sessionId && !run?.tracksInputs) void this.reportBackIfAwaited(e.sessionId, e, run?.completedAnswer);
|
|
112324
112973
|
if (e.type === "approval_resolved" && e.sessionId) this.raiseHostAsks(e.sessionId);
|
|
112974
|
+
if (endedTurn && e.sessionId && this.meta.get(e.sessionId)?.state !== "error") this.raiseHostAsks(e.sessionId);
|
|
112325
112975
|
if (endedTurn && e.sessionId) this.forgetCommits(e.sessionId);
|
|
112326
112976
|
if (endedTurn && e.sessionId) this.applyDeferredRestart(e.sessionId);
|
|
112977
|
+
if (e.type === "background_tasks" && e.sessionId) this.applyDeferredRestart(e.sessionId);
|
|
112327
112978
|
}
|
|
112328
|
-
/** A note
|
|
112979
|
+
/** A queued note not yet taken, on the delegation's wait or an observer's own reader (#646): the session is not between turns */
|
|
112980
|
+
inputsHeld(sessionId) {
|
|
112981
|
+
if (this.agentRuns.get(sessionId)?.waitingForInputs) return true;
|
|
112982
|
+
for (const observer of this.turnObservers.get(sessionId) ?? []) if (observer.waitingForInputs) return true;
|
|
112983
|
+
return false;
|
|
112984
|
+
}
|
|
112985
|
+
/**
|
|
112986
|
+
* A note receipt follows turn_complete: re-check the same deferred change when the last hold is released.
|
|
112987
|
+
*
|
|
112988
|
+
* Background work (#648). A change of safety (the permission preset, the launch settings, the tool definitions a
|
|
112989
|
+
* profile serves) is applied at the end of the turn whatever runs in the background: another turn under the old
|
|
112990
|
+
* policy is worse than a stopped dev server, and the conversation says what stopped (`noticeBackgroundStopped`). Any
|
|
112991
|
+
* other change (a model, an app set) waits while work the tool listed is running, as a sleep does, and is applied
|
|
112992
|
+
* anyway after `BACKGROUND_HOLD_MAX_MS`.
|
|
112993
|
+
*/
|
|
112329
112994
|
applyDeferredRestart(sessionId) {
|
|
112330
112995
|
const session2 = this.meta.get(sessionId);
|
|
112331
|
-
if (!session2 || inTurn(session2.state) || this.deferredRestarts.has(sessionId) || this.
|
|
112996
|
+
if (!session2 || inTurn(session2.state) || this.deferredRestarts.has(sessionId) || this.inputsHeld(sessionId) || !this.restartAfterTurn.has(sessionId)) return;
|
|
112997
|
+
const hold = this.backgroundHolds.get(sessionId);
|
|
112998
|
+
const holdExpired = !!hold && Date.now() - hold.since >= BACKGROUND_HOLD_MAX_MS;
|
|
112999
|
+
if (!holdExpired && !this.safetyRestartPending(sessionId) && this.backgroundRunning(sessionId)) {
|
|
113000
|
+
this.holdForBackground(sessionId);
|
|
113001
|
+
return;
|
|
113002
|
+
}
|
|
113003
|
+
this.restartAfterTurn.delete(sessionId);
|
|
113004
|
+
this.clearBackgroundHold(sessionId);
|
|
112332
113005
|
const apps = this.appsRestartAfterTurn.delete(sessionId);
|
|
112333
113006
|
const definitions = this.definitionsRestartAfterTurn.delete(sessionId);
|
|
112334
113007
|
const launchSettings = this.launchSettingsRestartAfterTurn.delete(sessionId);
|
|
@@ -112342,6 +113015,28 @@ var SessionManager = class {
|
|
|
112342
113015
|
void restarting.catch(() => {
|
|
112343
113016
|
});
|
|
112344
113017
|
}
|
|
113018
|
+
/** A deferred restart changes what the process may do (#648): its permission preset, launch settings or tool set */
|
|
113019
|
+
safetyRestartPending(sessionId) {
|
|
113020
|
+
const m = this.meta.get(sessionId);
|
|
113021
|
+
const live = this.running.get(sessionId);
|
|
113022
|
+
return this.launchSettingsRestartAfterTurn.has(sessionId) || this.definitionsRestartAfterTurn.has(sessionId) || !!m && !!live && live.permissionPreset !== m.permissionPreset;
|
|
113023
|
+
}
|
|
113024
|
+
/** Holds a non-safety restart for background work, at most `BACKGROUND_HOLD_MAX_MS` (#648) */
|
|
113025
|
+
holdForBackground(sessionId) {
|
|
113026
|
+
if (this.backgroundHolds.has(sessionId)) return;
|
|
113027
|
+
const timer = setTimeout(() => {
|
|
113028
|
+
console.error(`[sessions] ${sessionId.slice(0, 8)}: a restart waited ${Math.round(BACKGROUND_HOLD_MAX_MS / 6e4)} min for background work; applying it at the next quiet moment`);
|
|
113029
|
+
this.applyDeferredRestart(sessionId);
|
|
113030
|
+
}, BACKGROUND_HOLD_MAX_MS);
|
|
113031
|
+
timer.unref?.();
|
|
113032
|
+
this.backgroundHolds.set(sessionId, { since: Date.now(), timer });
|
|
113033
|
+
}
|
|
113034
|
+
/** The wait is over: the restart was applied, or the session is gone (#648) */
|
|
113035
|
+
clearBackgroundHold(sessionId) {
|
|
113036
|
+
const hold = this.backgroundHolds.get(sessionId);
|
|
113037
|
+
if (hold) clearTimeout(hold.timer);
|
|
113038
|
+
this.backgroundHolds.delete(sessionId);
|
|
113039
|
+
}
|
|
112345
113040
|
/**
|
|
112346
113041
|
* Notifies the orchestrator when the work it assigned finishes — **the other half of "one window".**
|
|
112347
113042
|
*
|
|
@@ -112356,8 +113051,8 @@ var SessionManager = class {
|
|
|
112356
113051
|
*/
|
|
112357
113052
|
async reportBackIfAwaited(sessionId, ending, answer) {
|
|
112358
113053
|
const orchestratorId = this.awaitingReport.get(sessionId);
|
|
112359
|
-
if (!orchestratorId) return;
|
|
112360
|
-
this.
|
|
113054
|
+
if (!orchestratorId || this.handingOver) return;
|
|
113055
|
+
this.clearReport(sessionId);
|
|
112361
113056
|
const target = this.meta.get(sessionId);
|
|
112362
113057
|
if (!target || !this.meta.has(orchestratorId)) return;
|
|
112363
113058
|
const project2 = target.projectId ? this.store.listProjects().find((p) => p.id === target.projectId)?.name ?? "(project no longer exists)" : "(none)";
|
|
@@ -112462,6 +113157,8 @@ If you need more, you can read that session's recent conversation with read_sess
|
|
|
112462
113157
|
break;
|
|
112463
113158
|
case "background_tasks":
|
|
112464
113159
|
m.backgroundTasks = applyBackgroundTasks(m.backgroundTasks, e);
|
|
113160
|
+
if (e.snapshot) this.backgroundUnknown.delete(m.id);
|
|
113161
|
+
if (!this.backgroundUnknown.has(m.id)) this.rememberBackgroundTasks(m);
|
|
112465
113162
|
break;
|
|
112466
113163
|
case "agent_version":
|
|
112467
113164
|
m.agentVersion = e.version;
|
|
@@ -112700,7 +113397,7 @@ If you need more, you can read that session's recent conversation with read_sess
|
|
|
112700
113397
|
async deliver(sessionId, text2, attachments, from, relayed, fromApp, fromAppVia = "inline", sendInput) {
|
|
112701
113398
|
const restarting = this.deferredRestarts.get(sessionId);
|
|
112702
113399
|
if (restarting) await restarting;
|
|
112703
|
-
if (this.
|
|
113400
|
+
if (this.worktreeCleanupBlocks(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
112704
113401
|
const m = this.meta.get(sessionId);
|
|
112705
113402
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
112706
113403
|
if (!this.handles.has(sessionId)) {
|
|
@@ -112711,7 +113408,7 @@ If you need more, you can read that session's recent conversation with read_sess
|
|
|
112711
113408
|
});
|
|
112712
113409
|
}
|
|
112713
113410
|
}
|
|
112714
|
-
if (this.
|
|
113411
|
+
if (this.worktreeCleanupBlocks(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
112715
113412
|
const h = this.requireHandle(sessionId);
|
|
112716
113413
|
this.closeStream(sessionId);
|
|
112717
113414
|
const seq = this.store.nextSeq(sessionId);
|
|
@@ -112729,7 +113426,7 @@ If you need more, you can read that session's recent conversation with read_sess
|
|
|
112729
113426
|
]);
|
|
112730
113427
|
m.lastSeq = seq;
|
|
112731
113428
|
m.lastReadSeq = seq;
|
|
112732
|
-
if (m.autoNamed && m.name === "New session" && !fromApp) {
|
|
113429
|
+
if (m.autoNamed && m.name === "New session" && !fromApp && !from) {
|
|
112733
113430
|
m.name = truncate(text2);
|
|
112734
113431
|
this.emit({ type: "session_title", sessionId, title: m.name, auto: true });
|
|
112735
113432
|
}
|
|
@@ -112766,7 +113463,7 @@ ${attachments.map((a) => `@${a.path}`).join("\n")}` : text2;
|
|
|
112766
113463
|
* untouched, and only the tool underneath it is swapped.
|
|
112767
113464
|
*/
|
|
112768
113465
|
async switchTool(sessionId, tool) {
|
|
112769
|
-
if (this.
|
|
113466
|
+
if (this.worktreeCleanupBlocks(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
112770
113467
|
const m = this.meta.get(sessionId);
|
|
112771
113468
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
112772
113469
|
if (m.tool === tool) return { ...m, live: this.handles.has(sessionId) };
|
|
@@ -112803,8 +113500,9 @@ ${attachments.map((a) => `@${a.path}`).join("\n")}` : text2;
|
|
|
112803
113500
|
const ask = this.hostAsks.get(requestId);
|
|
112804
113501
|
if (ask && ask.sessionId === sessionId) {
|
|
112805
113502
|
this.hostAsks.delete(requestId);
|
|
112806
|
-
const keepsAlways = ask.detail.kind === "project_access" || ask.detail.kind === "agent_start" || ask.detail.kind === "centralu_update";
|
|
113503
|
+
const keepsAlways = ask.detail.kind === "project_access" || ask.detail.kind === "agent_start" || ask.detail.kind === "centralu_update" || isCentraluApply(ask.detail);
|
|
112807
113504
|
const answer = !keepsAlways && decision === "always" ? "allow" : decision;
|
|
113505
|
+
this.restAfterCard(ask);
|
|
112808
113506
|
this.onEvent({ type: "approval_resolved", sessionId, requestId, decision: answer });
|
|
112809
113507
|
ask.resolve(ask.detail.kind === "agent_start" && answer === "always" && scope === "project" ? "always_project" : answer);
|
|
112810
113508
|
const after = this.meta.get(sessionId);
|
|
@@ -112980,6 +113678,37 @@ ${attachments.map((a) => `@${a.path}`).join("\n")}` : text2;
|
|
|
112980
113678
|
* opens would make that click feel slow.
|
|
112981
113679
|
*/
|
|
112982
113680
|
modelCache = /* @__PURE__ */ new Map();
|
|
113681
|
+
/** One read of a tool's list at a time: a second asker waits on it instead of starting another tool process */
|
|
113682
|
+
modelFetches = /* @__PURE__ */ new Map();
|
|
113683
|
+
/** When a read last failed, so a caller that cannot wait does not start another one at once (#660) */
|
|
113684
|
+
modelFailures = /* @__PURE__ */ new Map();
|
|
113685
|
+
/**
|
|
113686
|
+
* Reads the list once at a time and **keeps the result whenever it arrives**, also after the caller that asked has
|
|
113687
|
+
* stopped waiting: a Codex list that takes 20 seconds is cached for the next call instead of being thrown away at
|
|
113688
|
+
* the caller's bound and read again from scratch (#660). The read has its own ceiling, so it cannot stay pending
|
|
113689
|
+
* and block every later asker.
|
|
113690
|
+
*/
|
|
113691
|
+
fetchModels(tool, adapter) {
|
|
113692
|
+
const running = this.modelFetches.get(tool);
|
|
113693
|
+
if (running) return running;
|
|
113694
|
+
const p = withTimeout(adapter.listModels(), MODEL_LIST_READ_MS, `${tool}'s model list`).then(
|
|
113695
|
+
(models) => {
|
|
113696
|
+
this.modelCache.set(tool, { models, at: Date.now() });
|
|
113697
|
+
this.modelFailures.delete(tool);
|
|
113698
|
+
return models;
|
|
113699
|
+
},
|
|
113700
|
+
(err) => {
|
|
113701
|
+
this.modelFailures.set(tool, Date.now());
|
|
113702
|
+
throw err;
|
|
113703
|
+
}
|
|
113704
|
+
);
|
|
113705
|
+
this.modelFetches.set(tool, p);
|
|
113706
|
+
const clear = () => {
|
|
113707
|
+
if (this.modelFetches.get(tool) === p) this.modelFetches.delete(tool);
|
|
113708
|
+
};
|
|
113709
|
+
p.then(clear, clear);
|
|
113710
|
+
return p;
|
|
113711
|
+
}
|
|
112983
113712
|
async listModels(tool) {
|
|
112984
113713
|
const adapter = this.adapters.get(tool);
|
|
112985
113714
|
if (!adapter?.listModels) {
|
|
@@ -112988,13 +113717,40 @@ ${attachments.map((a) => `@${a.path}`).join("\n")}` : text2;
|
|
|
112988
113717
|
const hit = this.modelCache.get(tool);
|
|
112989
113718
|
if (hit && Date.now() - hit.at < 5 * 6e4) return { supported: true, models: hit.models };
|
|
112990
113719
|
try {
|
|
112991
|
-
const models = await withTimeout(
|
|
112992
|
-
this.modelCache.set(tool, { models, at: Date.now() });
|
|
113720
|
+
const models = await withTimeout(this.fetchModels(tool, adapter), 15e3);
|
|
112993
113721
|
return { supported: true, models };
|
|
112994
113722
|
} catch (err) {
|
|
112995
113723
|
return { supported: false, reason: err.message, models: hit?.models ?? [] };
|
|
112996
113724
|
}
|
|
112997
113725
|
}
|
|
113726
|
+
/**
|
|
113727
|
+
* The list for checking a choice, **never waiting longer than `ms`** (#660). A list we already have, however old,
|
|
113728
|
+
* answers at once (and an old one is refreshed in the background); otherwise one read is started or joined and
|
|
113729
|
+
* waited on for `ms`. Null when there is no list in time: the caller goes on without checking. A read that fails
|
|
113730
|
+
* is not started again for a minute, so a hung Codex does not get a new tool process on every call.
|
|
113731
|
+
*
|
|
113732
|
+
* `fresh` skips the cached list and reads again: the CLI may have been updated since, so a name the cached list
|
|
113733
|
+
* lacks is not refused on its word alone. `cached` says which kind of list came back.
|
|
113734
|
+
*/
|
|
113735
|
+
async modelsWithin(tool, ms3, fresh = false) {
|
|
113736
|
+
const adapter = this.adapters.get(tool);
|
|
113737
|
+
if (!adapter?.listModels) return null;
|
|
113738
|
+
const hit = this.modelCache.get(tool);
|
|
113739
|
+
const stale = !hit || Date.now() - hit.at >= 5 * 6e4;
|
|
113740
|
+
if (hit && !stale && !fresh) return { models: hit.models, cached: true };
|
|
113741
|
+
const failedAt = this.modelFailures.get(tool);
|
|
113742
|
+
const failedRecently = failedAt !== void 0 && Date.now() - failedAt < MODEL_LIST_RETRY_MS && !this.modelFetches.has(tool);
|
|
113743
|
+
if (failedRecently) return hit && !fresh ? { models: hit.models, cached: true } : null;
|
|
113744
|
+
const read = this.fetchModels(tool, adapter);
|
|
113745
|
+
read.catch(() => {
|
|
113746
|
+
});
|
|
113747
|
+
if (hit && !fresh) return { models: hit.models, cached: true };
|
|
113748
|
+
try {
|
|
113749
|
+
return { models: await withTimeout(read, ms3), cached: false };
|
|
113750
|
+
} catch {
|
|
113751
|
+
return null;
|
|
113752
|
+
}
|
|
113753
|
+
}
|
|
112998
113754
|
/** A project's working directory. Used by the terminal to decide its own key (cwd) */
|
|
112999
113755
|
cwdOfProject(projectId) {
|
|
113000
113756
|
return this.cwdOf(projectId);
|
|
@@ -113431,13 +114187,14 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113431
114187
|
* reports background work.
|
|
113432
114188
|
*/
|
|
113433
114189
|
sessionIdle(sessionId) {
|
|
113434
|
-
if (this.
|
|
114190
|
+
if (this.worktreeCleanupBlocks(sessionId)) return { idle: false, reason: "turn" };
|
|
113435
114191
|
const m = this.meta.get(sessionId);
|
|
113436
114192
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
113437
|
-
if (this.
|
|
114193
|
+
if (this.inputsHeld(sessionId)) return { idle: false, reason: "turn" };
|
|
113438
114194
|
return sessionIdle(m, {
|
|
113439
114195
|
running: this.handles.has(sessionId),
|
|
113440
|
-
|
|
114196
|
+
// An adopted process whose work the previous host did not record is as silent as a tool that cannot report (#648)
|
|
114197
|
+
reportsBackground: !!this.adapters.get(m.tool)?.capabilities.backgroundTasks && this.backgroundKnown(sessionId)
|
|
113441
114198
|
});
|
|
113442
114199
|
}
|
|
113443
114200
|
/**
|
|
@@ -113497,7 +114254,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113497
114254
|
* process.
|
|
113498
114255
|
*/
|
|
113499
114256
|
async restartSession(sessionId, cause2) {
|
|
113500
|
-
if (this.
|
|
114257
|
+
if (this.worktreeCleanupBlocks(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
113501
114258
|
if (cause2) {
|
|
113502
114259
|
this.restartReasons.set(sessionId, cause2);
|
|
113503
114260
|
if (cause2 === "launch_settings") console.error(`[sessions] restarting ${sessionId}: launch settings changed or previously unrecorded`);
|
|
@@ -113506,8 +114263,11 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113506
114263
|
this.appsRestartAfterTurn.delete(sessionId);
|
|
113507
114264
|
this.definitionsRestartAfterTurn.delete(sessionId);
|
|
113508
114265
|
this.launchSettingsRestartAfterTurn.delete(sessionId);
|
|
114266
|
+
this.noticeTrustRestart(sessionId);
|
|
114267
|
+
this.clearBackgroundHold(sessionId);
|
|
113509
114268
|
const h = this.handles.get(sessionId);
|
|
113510
114269
|
if (h) {
|
|
114270
|
+
this.noticeBackgroundStopped(sessionId, cause2);
|
|
113511
114271
|
await h.dispose().catch(() => {
|
|
113512
114272
|
});
|
|
113513
114273
|
this.stampExternalSynced(sessionId);
|
|
@@ -113515,7 +114275,17 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113515
114275
|
this.running.delete(sessionId);
|
|
113516
114276
|
this.closeStream(sessionId);
|
|
113517
114277
|
}
|
|
113518
|
-
|
|
114278
|
+
const dropped = (reason) => {
|
|
114279
|
+
if (!h || this.handles.has(sessionId)) return;
|
|
114280
|
+
console.error(`[sessions] restart of ${sessionId.slice(0, 8)} did not bring a process up: ${reason}`);
|
|
114281
|
+
this.emit({ type: "session_live", sessionId, live: false, reason: "restart_failed" });
|
|
114282
|
+
};
|
|
114283
|
+
const r = await this.resumeSession(sessionId).catch((err) => {
|
|
114284
|
+
dropped(err.message);
|
|
114285
|
+
throw err;
|
|
114286
|
+
});
|
|
114287
|
+
if (!r.resumed) dropped(r.reason ?? "unknown");
|
|
114288
|
+
return r;
|
|
113519
114289
|
}
|
|
113520
114290
|
/** The persisted prefix a process was started with. It is separate from SessionInfo: a screen never needs it. */
|
|
113521
114291
|
toolPrefixKey(sessionId) {
|
|
@@ -113587,8 +114357,9 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113587
114357
|
* environment). The adapter owns the fingerprint; the host only gives it the same safe
|
|
113588
114358
|
* boundary behaviour as changed tool definitions.
|
|
113589
114359
|
*/
|
|
113590
|
-
async refreshLaunchSettings() {
|
|
113591
|
-
for (const session2 of this.meta.values()) {
|
|
114360
|
+
async refreshLaunchSettings(projectId, trustChange = false) {
|
|
114361
|
+
for (const session2 of [...this.meta.values()]) {
|
|
114362
|
+
if (projectId !== void 0 && session2.projectId !== projectId) continue;
|
|
113592
114363
|
const cwd = this.store.sessionCwd(session2.id);
|
|
113593
114364
|
if (!cwd) continue;
|
|
113594
114365
|
const next = this.launchSettingsFingerprint(session2, cwd);
|
|
@@ -113597,6 +114368,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113597
114368
|
this.store.setAppSetting(this.launchSettingsKey(session2.id), next);
|
|
113598
114369
|
continue;
|
|
113599
114370
|
}
|
|
114371
|
+
if (trustChange) this.trustRestartNotice.add(session2.id);
|
|
113600
114372
|
if (inTurn(session2.state)) {
|
|
113601
114373
|
this.restartAfterTurn.add(session2.id);
|
|
113602
114374
|
this.launchSettingsRestartAfterTurn.add(session2.id);
|
|
@@ -113621,7 +114393,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113621
114393
|
const off = () => !this.uiPreferences().sessionTools;
|
|
113622
114394
|
const offError = "Turned off in Settings";
|
|
113623
114395
|
return {
|
|
113624
|
-
updateCentralu: async (action) => off() ? { ok: false, error: offError } : this.updateCentraluAsked(sessionId, action),
|
|
114396
|
+
updateCentralu: async (action, signal) => off() ? { ok: false, error: offError } : this.updateCentraluAsked(sessionId, action, signal),
|
|
113625
114397
|
listSessions: async () => off() ? [] : view2.listSessions(),
|
|
113626
114398
|
readSession: async (...a) => off() ? { ok: false, error: offError } : view2.readSession(...a),
|
|
113627
114399
|
recall: async (...a) => off() ? { hits: [] } : view2.recall(...a),
|
|
@@ -113667,7 +114439,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113667
114439
|
);
|
|
113668
114440
|
const scopeError = (id2) => scopeIds !== void 0 ? `Not a member of this coordinating session: ${id2}` : ownProject ? `Not a session in this project: ${id2}` : hubView ? `Not a session this machine shares: ${id2}` : `Not a session this app manages: ${id2}`;
|
|
113669
114441
|
return {
|
|
113670
|
-
updateCentralu: async (action) => this.updateCentraluAsked(orchestratorId, action),
|
|
114442
|
+
updateCentralu: async (action, signal) => this.updateCentraluAsked(orchestratorId, action, signal),
|
|
113671
114443
|
listSessions: async () => {
|
|
113672
114444
|
const byId = projects();
|
|
113673
114445
|
const mine = [...this.meta.values()].filter((s) => s.id !== orchestratorId && inScope(s)).map((s) => ({
|
|
@@ -113700,15 +114472,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113700
114472
|
const target = this.meta.get(sessionId);
|
|
113701
114473
|
if (!target || !inScope(target)) return { ok: false, error: scopeError(sessionId) };
|
|
113702
114474
|
try {
|
|
113703
|
-
const info = await this.updateSettings(sessionId, s);
|
|
113704
|
-
this.emit({
|
|
113705
|
-
type: "settings_changed",
|
|
113706
|
-
sessionId,
|
|
113707
|
-
model: info.model,
|
|
113708
|
-
effort: info.effort,
|
|
113709
|
-
verbosity: info.verbosity,
|
|
113710
|
-
serviceTier: info.serviceTier
|
|
113711
|
-
});
|
|
114475
|
+
const info = await this.updateSettings(sessionId, s, "orchestrator");
|
|
113712
114476
|
return { ok: true, ...info.applied === "after_turn" ? { deferred: true } : {} };
|
|
113713
114477
|
} catch (e) {
|
|
113714
114478
|
return { ok: false, error: e.message };
|
|
@@ -113864,8 +114628,8 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113864
114628
|
sessionId: orchestratorId,
|
|
113865
114629
|
name: orch?.name ?? "Orchestrator"
|
|
113866
114630
|
});
|
|
113867
|
-
if (reportBack) this.
|
|
113868
|
-
else if (this.awaitingReport.get(sessionId) === orchestratorId) this.
|
|
114631
|
+
if (reportBack) this.markReport(sessionId, orchestratorId);
|
|
114632
|
+
else if (this.awaitingReport.get(sessionId) === orchestratorId) this.clearReport(sessionId);
|
|
113869
114633
|
return { ok: true };
|
|
113870
114634
|
} catch (e) {
|
|
113871
114635
|
return { ok: false, error: e.message };
|
|
@@ -114130,15 +114894,19 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
114130
114894
|
if (boundary) console.error(`[permissions] auto session ${sessionId}: ${boundary} allowed`);
|
|
114131
114895
|
return true;
|
|
114132
114896
|
}
|
|
114133
|
-
/**
|
|
114134
|
-
|
|
114897
|
+
/**
|
|
114898
|
+
* Auto allows and logs; other presets raise a host card, withdrawn if the signal fires. `requestId` is given when the
|
|
114899
|
+
* caller tracks the card itself (a start_agent request, #673); `shown` raises one the conversation already records
|
|
114900
|
+
* (a request carried over from the previous host) without recording it again.
|
|
114901
|
+
*/
|
|
114902
|
+
askHost(sessionId, detail, signal, card = {}) {
|
|
114135
114903
|
if (signal?.aborted) return Promise.resolve(null);
|
|
114136
114904
|
const note = detail.kind === "project_access" && !detail.to.trusted ? " (target not trusted)" : "";
|
|
114137
114905
|
if (this.autoBoundaryOpen(sessionId, `${detail.kind}${note}`)) return Promise.resolve("allow");
|
|
114138
114906
|
return new Promise((resolve12) => {
|
|
114139
114907
|
if (signal?.aborted) return resolve12(null);
|
|
114140
|
-
const requestId = `${detail.kind === "project_access" ? "xp" : detail.kind === "agent_start" ? "ag" : detail.kind === "centralu_update" ? "up" : "mk"}-${randomUUID10()}`;
|
|
114141
|
-
this.hostAsks.set(requestId, { requestId, sessionId, detail, shown: false, resolve: resolve12 });
|
|
114908
|
+
const requestId = card.requestId ?? `${detail.kind === "project_access" ? "xp" : detail.kind === "agent_start" ? "ag" : detail.kind === "centralu_update" || isCentraluApply(detail) ? "up" : "mk"}-${randomUUID10()}`;
|
|
114909
|
+
this.hostAsks.set(requestId, { requestId, sessionId, detail, shown: card.shown ?? false, resolve: resolve12 });
|
|
114142
114910
|
signal?.addEventListener("abort", () => {
|
|
114143
114911
|
if (!this.hostAsks.delete(requestId)) return;
|
|
114144
114912
|
const m = this.meta.get(sessionId);
|
|
@@ -114335,17 +115103,20 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114335
115103
|
this.delegations.set(key, d2);
|
|
114336
115104
|
return d2;
|
|
114337
115105
|
}
|
|
114338
|
-
/**
|
|
114339
|
-
|
|
114340
|
-
|
|
115106
|
+
/**
|
|
115107
|
+
* Read an existing turn without claiming it in delegations, agentRuns or the caller's sleep graph. With `own`, a
|
|
115108
|
+
* fresh reader even while another caller's wait is active: a note's receipt is held on it, never on that wait (#646).
|
|
115109
|
+
*/
|
|
115110
|
+
observeTurn(child, subscribe, own = false) {
|
|
115111
|
+
const active = own ? void 0 : this.agentRuns.get(child.id);
|
|
114341
115112
|
const shared = active && !active.finished ? active : void 0;
|
|
114342
|
-
|
|
114343
|
-
|
|
114344
|
-
|
|
114345
|
-
() =>
|
|
114346
|
-
|
|
114347
|
-
|
|
114348
|
-
);
|
|
115113
|
+
let answerAfter = this.meta.get(child.id)?.lastSeq ?? 0;
|
|
115114
|
+
const wait = shared ?? new AgentRunWait(() => {
|
|
115115
|
+
}, () => child.name, void 0, own ? () => {
|
|
115116
|
+
const answer = finalAnswer(this.store.loadMessages(child.id, 50).filter((m) => m.seq > answerAfter));
|
|
115117
|
+
answerAfter = this.meta.get(child.id)?.lastSeq ?? answerAfter;
|
|
115118
|
+
return answer;
|
|
115119
|
+
} : () => finalAnswer(this.store.loadMessages(child.id, 50)));
|
|
114349
115120
|
const project2 = this.store.listProjects().find((p) => p.id === child.projectId);
|
|
114350
115121
|
const target = { id: child.projectId ?? "", name: project2?.name ?? "", path: this.store.sessionCwd(child.id) ?? project2?.path ?? "" };
|
|
114351
115122
|
if (!shared && subscribe) {
|
|
@@ -114443,7 +115214,8 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114443
115214
|
* 1. Who may: an ordinary session of a trusted project (the rule ask_project and attach_app follow, #382), and
|
|
114444
115215
|
* not one that was itself started or asked by another session (`askedBy`): depth one.
|
|
114445
115216
|
* 2. How many: at most `AGENT_CEILING` children run at once in a project, over all its sessions.
|
|
114446
|
-
* 3. The person's consent (`ensureAgentConsent`):
|
|
115217
|
+
* 3. The person's consent (`ensureAgentConsent`): the project's trust for a `normal` caller (#673); otherwise a
|
|
115218
|
+
* card, answered once, always for this session, or always for this project.
|
|
114447
115219
|
* 4. The child: a visible session in the caller's folder, marked `askedBy` the caller, on the tool, model and
|
|
114448
115220
|
* effort asked for (or the project's own, as a session the person opens there), under **the caller's own
|
|
114449
115221
|
* permission preset**: never more than the caller has, and nothing the caller can change afterwards.
|
|
@@ -114452,49 +115224,184 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114452
115224
|
* "working" and the caller is told when the turn ends (the reportBack path); agent_session waits again.
|
|
114453
115225
|
*/
|
|
114454
115226
|
async startAgent(callerId, opts, signal) {
|
|
114455
|
-
const
|
|
114456
|
-
|
|
114457
|
-
|
|
114458
|
-
|
|
115227
|
+
const startedAt = Date.now();
|
|
115228
|
+
const progress = { stage: "checking" };
|
|
115229
|
+
const ctl = new AbortController();
|
|
115230
|
+
const onAbort = () => ctl.abort();
|
|
115231
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
115232
|
+
if (signal?.aborted) ctl.abort();
|
|
115233
|
+
let timer;
|
|
115234
|
+
const late = new Promise((resolve12) => {
|
|
115235
|
+
timer = setTimeout(() => {
|
|
115236
|
+
const standing = progress.stage === "asking" && progress.requestId ? this.agentStarts.get(progress.requestId) : void 0;
|
|
115237
|
+
if (standing && !standing.ended) {
|
|
115238
|
+
standing.detached = true;
|
|
115239
|
+
resolve12(agentStartPending(standing.req));
|
|
115240
|
+
return;
|
|
115241
|
+
}
|
|
115242
|
+
ctl.abort();
|
|
115243
|
+
const d8 = progress.stage === "created" ? progress.d : void 0;
|
|
115244
|
+
if (d8) {
|
|
115245
|
+
const notice2 = [progress.unchecked, this.handles.get(d8.sessionId)?.delegationNotice?.()].filter(Boolean).join("; ");
|
|
115246
|
+
const working = this.agentWorking(d8);
|
|
115247
|
+
resolve12(working.ok && notice2 ? { ...working, notice: [working.notice, notice2].filter(Boolean).join("; ") } : working);
|
|
115248
|
+
} else resolve12(startAgentLate(progress));
|
|
115249
|
+
}, START_AGENT_SETUP_MS);
|
|
115250
|
+
});
|
|
115251
|
+
let set3;
|
|
115252
|
+
const setup = this.setUpAgent(callerId, opts, ctl.signal, progress);
|
|
115253
|
+
try {
|
|
115254
|
+
set3 = await Promise.race([setup, late.then((r) => ({ done: r }))]);
|
|
115255
|
+
} finally {
|
|
115256
|
+
clearTimeout(timer);
|
|
115257
|
+
signal?.removeEventListener("abort", onAbort);
|
|
114459
115258
|
}
|
|
115259
|
+
if ("done" in set3) {
|
|
115260
|
+
void setup.then((r) => {
|
|
115261
|
+
if ("key" in r) this.agentWorking(r.d);
|
|
115262
|
+
}, () => {
|
|
115263
|
+
});
|
|
115264
|
+
return set3.done;
|
|
115265
|
+
}
|
|
115266
|
+
const { key, d: d2, notice } = set3;
|
|
115267
|
+
const left = Math.max(1e3, START_AGENT_CALL_MS - (Date.now() - startedAt));
|
|
115268
|
+
const result = opts.wait === false ? this.agentWorking(d2) : await this.awaitAgent(key, d2, signal, false, Math.min(ASK_WAIT_MS, left));
|
|
115269
|
+
return result.ok && notice ? { ...result, notice: [result.notice, notice].filter(Boolean).join("; ") } : result;
|
|
115270
|
+
}
|
|
115271
|
+
/** Everything start_agent does before it waits on the child's turn (#660); `done` is a final answer, otherwise the child works */
|
|
115272
|
+
async setUpAgent(callerId, opts, signal, progress) {
|
|
115273
|
+
const done = (r5) => ({ done: r5 });
|
|
115274
|
+
const caller = this.meta.get(callerId);
|
|
115275
|
+
if (!caller?.projectId) return done({ ok: false, error: "This session belongs to no project, so it cannot start agents." });
|
|
115276
|
+
if (this.agentDepthBlocked(caller) && !this.autoBoundaryOpen(callerId, "delegation depth")) return done({ ok: false, error: AGENT_DEPTH_ERROR });
|
|
114460
115277
|
const project2 = this.store.listProjects().find((p) => p.id === caller.projectId);
|
|
114461
|
-
if (!project2) return { ok: false, error: "This session's project is no longer registered." };
|
|
115278
|
+
if (!project2) return done({ ok: false, error: "This session's project is no longer registered." });
|
|
114462
115279
|
if (project2.trusted !== true && !this.autoBoundaryOpen(callerId, "start in an untrusted project")) {
|
|
114463
|
-
return { ok: false, error:
|
|
115280
|
+
return done({ ok: false, error: `This session's project is not trusted, so it cannot start agents \u2014 the person can trust it from the project menu. ${ASK_THE_PERSON}` });
|
|
114464
115281
|
}
|
|
115282
|
+
const waiting = [...this.agentStarts.values()].find(
|
|
115283
|
+
(r5) => !r5.ended && r5.req.callerId === callerId && r5.req.task === opts.task && (!opts.tool || r5.req.tool === opts.tool)
|
|
115284
|
+
);
|
|
115285
|
+
if (waiting) return done(agentStartPending(waiting.req));
|
|
114465
115286
|
const tool = opts.tool ?? this.firstTool(project2.defaultTool);
|
|
114466
|
-
if (!tool) return { ok: false, error: "No agent adapter is available. Install and configure an agent CLI before starting an agent." };
|
|
115287
|
+
if (!tool) return done({ ok: false, error: "No agent adapter is available. Install and configure an agent CLI before starting an agent." });
|
|
114467
115288
|
const adapter = this.adapters.get(tool);
|
|
114468
|
-
if (!adapter) return { ok: false, error: `No agent named "${tool}" here. Installed tools: ${[...this.adapters.keys()].join(", ") || "none"}.` };
|
|
115289
|
+
if (!adapter) return done({ ok: false, error: `No agent named "${tool}" here. Installed tools: ${[...this.adapters.keys()].join(", ") || "none"}.` });
|
|
114469
115290
|
const full = this.agentCeilingError(callerId, project2.id);
|
|
114470
|
-
if (full) return { ok: false, error: full };
|
|
115291
|
+
if (full) return done({ ok: false, error: full });
|
|
114471
115292
|
const remembered = this.store.projectToolDefaults(project2.id)[tool];
|
|
114472
115293
|
const model = opts.model ?? remembered?.model ?? void 0;
|
|
114473
115294
|
const effort = opts.effort ?? (!opts.model || opts.model === remembered?.model ? remembered?.effort ?? void 0 : void 0);
|
|
114474
|
-
const
|
|
114475
|
-
if (
|
|
115295
|
+
const checked = await this.agentChoiceError(tool, opts.model, opts.effort, model);
|
|
115296
|
+
if (checked.error) return done({ ok: false, error: checked.error });
|
|
115297
|
+
const unchecked = checked.unchecked;
|
|
115298
|
+
progress.unchecked = unchecked;
|
|
114476
115299
|
const found = await adapter.detect();
|
|
114477
|
-
if (!found.installed || !found.loggedIn) return { ok: false, error: `${adapter.descriptor.label} cannot take the task: ${found.detail}` };
|
|
115300
|
+
if (!found.installed || !found.loggedIn) return done({ ok: false, error: `${adapter.descriptor.label} cannot take the task: ${found.detail}. ${ASK_THE_PERSON}` });
|
|
114478
115301
|
const worktree = opts.worktree || opts.branch ? await this.agentWorktreeBranch(project2, opts) : null;
|
|
114479
|
-
if (worktree && "error" in worktree) return { ok: false, error: worktree.error };
|
|
114480
|
-
|
|
114481
|
-
|
|
114482
|
-
|
|
114483
|
-
|
|
114484
|
-
|
|
114485
|
-
|
|
114486
|
-
|
|
114487
|
-
|
|
114488
|
-
|
|
114489
|
-
|
|
114490
|
-
|
|
114491
|
-
|
|
114492
|
-
|
|
114493
|
-
|
|
115302
|
+
if (worktree && "error" in worktree) return done({ ok: false, error: worktree.error });
|
|
115303
|
+
progress.stage = "asking";
|
|
115304
|
+
const detail = {
|
|
115305
|
+
kind: "agent_start",
|
|
115306
|
+
project: { id: project2.id, name: project2.name },
|
|
115307
|
+
tool: adapter.descriptor.label,
|
|
115308
|
+
...model ? { model } : {},
|
|
115309
|
+
preset: caller.permissionPreset,
|
|
115310
|
+
...worktree ? { worktree: { branch: worktree.branch } } : {},
|
|
115311
|
+
text: taskLine(opts.task)
|
|
115312
|
+
};
|
|
115313
|
+
const choice = {
|
|
115314
|
+
tool,
|
|
115315
|
+
...model ? { model } : {},
|
|
115316
|
+
...effort ? { effort } : {},
|
|
115317
|
+
...worktree ? { worktreeBranch: worktree.branch } : {},
|
|
115318
|
+
task: opts.task,
|
|
115319
|
+
...unchecked ? { unchecked } : {}
|
|
115320
|
+
};
|
|
115321
|
+
let request;
|
|
115322
|
+
const consent = await this.ensureAgentConsent(callerId, detail, signal, (requestId) => {
|
|
115323
|
+
request = { requestId, callerId, projectId: project2.id, ...choice, detail };
|
|
115324
|
+
this.openAgentStart(request);
|
|
115325
|
+
progress.requestId = requestId;
|
|
115326
|
+
});
|
|
115327
|
+
if (!consent.ok) {
|
|
115328
|
+
if (request && consent.why === "withdrawn" && (this.handingOver || this.disposing)) return done(agentStartPending(request));
|
|
115329
|
+
if (request) {
|
|
115330
|
+
const ended = this.agentStarts.get(request.requestId)?.ended;
|
|
115331
|
+
if (ended && "error" in ended) return done({ ok: false, error: ended.error });
|
|
115332
|
+
const how = consent.why === "denied" ? "denied" : signal.aborted ? "cancelled" : "withdrawn";
|
|
115333
|
+
this.endAgentStart(request.requestId, { error: consent.error, person: agentStartPersonText(request, how), denied: consent.why === "denied" });
|
|
115334
|
+
}
|
|
115335
|
+
return done(consent);
|
|
115336
|
+
}
|
|
115337
|
+
if (signal.aborted) {
|
|
115338
|
+
const error51 = "The call was withdrawn before the session was created, so none was.";
|
|
115339
|
+
if (request) this.endAgentStart(request.requestId, { error: error51, person: agentStartPersonText(request, "cancelled") });
|
|
115340
|
+
return done({ ok: false, error: error51 });
|
|
115341
|
+
}
|
|
115342
|
+
const r = await this.createAgentChild(callerId, { id: project2.id, name: project2.name, path: project2.path }, choice, progress, request?.detail.preset);
|
|
115343
|
+
if (request) {
|
|
115344
|
+
if ("key" in r) this.endAgentStart(request.requestId, { childId: r.d.sessionId });
|
|
115345
|
+
else {
|
|
115346
|
+
const error51 = r.done.ok ? "No session was created." : r.done.error;
|
|
115347
|
+
this.endAgentStart(request.requestId, { error: error51, person: agentStartPersonText(request, "failed", error51) });
|
|
115348
|
+
}
|
|
115349
|
+
}
|
|
115350
|
+
return r;
|
|
115351
|
+
}
|
|
115352
|
+
/**
|
|
115353
|
+
* The child itself, once the person (or the project's trust) allowed it (#561, #673): counted again, made in the
|
|
115354
|
+
* caller's folder or a new worktree, named, and given the task. A card may have stood for hours, so what the start
|
|
115355
|
+
* rests on is read again first (#673 review): the caller is still here, its project still trusted, it still may start
|
|
115356
|
+
* agents (depth one), and its preset is no higher than the one the person read on the card (`approvedPreset`).
|
|
115357
|
+
*
|
|
115358
|
+
* **The place is taken before the first wait** (#673 review): the ceiling is checked and a place reserved in one
|
|
115359
|
+
* synchronous step (`agentStartReserved`), held until the child's delegation counts it or the start fails. Checked
|
|
115360
|
+
* and counted on either side of `createSession`, eight calls at once all passed a ceiling of four.
|
|
115361
|
+
*/
|
|
115362
|
+
async createAgentChild(callerId, project2, choice, progress, approvedPreset) {
|
|
115363
|
+
const done = (r) => ({ done: r });
|
|
115364
|
+
const caller = this.meta.get(callerId);
|
|
115365
|
+
if (!caller) return done({ ok: false, error: "The session that asked is no longer here." });
|
|
115366
|
+
const now = this.store.listProjects().find((p) => p.id === project2.id);
|
|
115367
|
+
if (!now) return done({ ok: false, error: "This session's project is no longer registered." });
|
|
115368
|
+
const auto = caller.permissionPreset === "auto";
|
|
115369
|
+
if (now.trusted !== true && !auto) {
|
|
115370
|
+
return done({ ok: false, error: `This session's project is no longer trusted, so it cannot start agents. ${ASK_THE_PERSON}` });
|
|
115371
|
+
}
|
|
115372
|
+
if (this.agentDepthBlocked(caller) && !auto) return done({ ok: false, error: AGENT_DEPTH_ERROR });
|
|
115373
|
+
if (approvedPreset && !presetWithin(caller.permissionPreset, approvedPreset)) {
|
|
115374
|
+
return done({
|
|
115375
|
+
ok: false,
|
|
115376
|
+
error: `This session's permissions were raised to ${caller.permissionPreset} after the person allowed a session with ${approvedPreset}, so none was started. Call start_agent again if it is still needed: the person decides on the new permissions. ${ASK_THE_PERSON}`
|
|
115377
|
+
});
|
|
115378
|
+
}
|
|
115379
|
+
const { tool, model, effort, unchecked } = choice;
|
|
115380
|
+
const worktree = choice.worktreeBranch ? { branch: choice.worktreeBranch } : null;
|
|
114494
115381
|
const fullNow = this.agentCeilingError(callerId, project2.id);
|
|
114495
|
-
if (fullNow) return { ok: false, error: fullNow };
|
|
115382
|
+
if (fullNow) return done({ ok: false, error: fullNow });
|
|
115383
|
+
this.agentStartReserved.set(project2.id, (this.agentStartReserved.get(project2.id) ?? 0) + 1);
|
|
115384
|
+
let reserved = true;
|
|
115385
|
+
const release = () => {
|
|
115386
|
+
if (!reserved) return;
|
|
115387
|
+
reserved = false;
|
|
115388
|
+
const left = (this.agentStartReserved.get(project2.id) ?? 1) - 1;
|
|
115389
|
+
if (left > 0) this.agentStartReserved.set(project2.id, left);
|
|
115390
|
+
else this.agentStartReserved.delete(project2.id);
|
|
115391
|
+
};
|
|
115392
|
+
try {
|
|
115393
|
+
return await this.createReservedChild(callerId, caller, project2, { tool, model, effort, unchecked, task: choice.task }, worktree, progress, release);
|
|
115394
|
+
} finally {
|
|
115395
|
+
release();
|
|
115396
|
+
}
|
|
115397
|
+
}
|
|
115398
|
+
/** The rest of `createAgentChild`, with its place reserved; `release` once the delegation counts the child */
|
|
115399
|
+
async createReservedChild(callerId, caller, project2, choice, worktree, progress, release) {
|
|
115400
|
+
const done = (r) => ({ done: r });
|
|
115401
|
+
const { tool, model, effort, unchecked } = choice;
|
|
114496
115402
|
const cwd = worktree ? project2.path : this.store.sessionCwd(callerId) ?? caller.worktree?.path ?? project2.path;
|
|
114497
115403
|
let child;
|
|
115404
|
+
progress.stage = "creating";
|
|
114498
115405
|
try {
|
|
114499
115406
|
child = await this.createSession({
|
|
114500
115407
|
projectId: project2.id,
|
|
@@ -114512,21 +115419,24 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114512
115419
|
...worktree ? { worktree: true, worktreeBranch: worktree.branch, ...caller.worktree ? { worktreeBase: caller.worktree.branch } : {} } : {}
|
|
114513
115420
|
});
|
|
114514
115421
|
} catch (e) {
|
|
114515
|
-
return { ok: false, error: `Could not start the session: ${e.message}` };
|
|
115422
|
+
return done({ ok: false, error: `Could not start the session: ${e.message}` });
|
|
114516
115423
|
}
|
|
114517
|
-
|
|
115424
|
+
progress.stage = "created";
|
|
115425
|
+
progress.childId = child.id;
|
|
115426
|
+
if (!worktree) this.rename(child.id, agentName(choice.task));
|
|
114518
115427
|
const key = `${callerId}>${child.id}`;
|
|
114519
115428
|
const d2 = this.startDelegation(key, callerId, child.id, { id: project2.id, name: project2.name, path: child.worktree?.path ?? cwd }, true);
|
|
115429
|
+
release();
|
|
115430
|
+
progress.d = d2;
|
|
114520
115431
|
try {
|
|
114521
|
-
await this.deliver(child.id, agentFrame(labelOf(this.store, caller),
|
|
115432
|
+
await this.deliver(child.id, agentFrame(labelOf(this.store, caller), choice.task), void 0, { sessionId: callerId, name: labelOf(this.store, caller) }, false);
|
|
114522
115433
|
} catch (e) {
|
|
114523
115434
|
this.delegations.delete(key);
|
|
114524
115435
|
if (this.agentRuns.get(child.id) === d2.wait) this.agentRuns.delete(child.id);
|
|
114525
|
-
return { ok: false, error: `Could not give the task to the new session: ${e.message}` };
|
|
115436
|
+
return done({ ok: false, error: `Could not give the task to the new session: ${e.message}` });
|
|
114526
115437
|
}
|
|
114527
115438
|
const notice = this.handles.get(child.id)?.delegationNotice?.();
|
|
114528
|
-
|
|
114529
|
-
return result.ok && notice ? { ...result, notice: [result.notice, notice].filter(Boolean).join("; ") } : result;
|
|
115439
|
+
return { key, d: d2, notice: [unchecked, notice].filter(Boolean).join("; ") || void 0 };
|
|
114530
115440
|
}
|
|
114531
115441
|
/**
|
|
114532
115442
|
* agent_session (#561): safe/normal work only with their own children; auto opens ownership within the project.
|
|
@@ -114542,6 +115452,17 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114542
115452
|
if (opts.sessionId === callerId && opts.action !== "status") {
|
|
114543
115453
|
return { ok: false, error: "agent_session cannot wait on, stop, delete or send to its own calling session." };
|
|
114544
115454
|
}
|
|
115455
|
+
const request = this.agentStarts.get(opts.sessionId);
|
|
115456
|
+
if (request && request.req.callerId === callerId) {
|
|
115457
|
+
const r = await this.agentStartSession(request, opts.action, signal);
|
|
115458
|
+
if ("ok" in r) return r;
|
|
115459
|
+
opts = { ...opts, sessionId: r.childId, action: r.action };
|
|
115460
|
+
} else if (opts.sessionId.startsWith("ag-") && !this.meta.has(opts.sessionId)) {
|
|
115461
|
+
return {
|
|
115462
|
+
ok: false,
|
|
115463
|
+
error: `No start_agent request ${opts.sessionId} of this session is open here: it ended before Centralu restarted, or it is not this session's. read_session (without a sessionId) lists the sessions.`
|
|
115464
|
+
};
|
|
115465
|
+
}
|
|
114545
115466
|
const caller = this.meta.get(callerId);
|
|
114546
115467
|
const child = this.meta.get(opts.sessionId);
|
|
114547
115468
|
const deletingWithoutWorktree = opts.action === "delete" && !child?.worktree;
|
|
@@ -114551,6 +115472,8 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114551
115472
|
}
|
|
114552
115473
|
if (opts.sessionId === callerId) return this.agentAtRest(child);
|
|
114553
115474
|
const key = `${callerId}>${child.id}`;
|
|
115475
|
+
const note = this.delegations.has(key) ? void 0 : this.observerNotes.get(key);
|
|
115476
|
+
if (note && (opts.action === "status" || opts.action === "wait")) return this.readObserverNote(key, note, opts.action === "wait", signal);
|
|
114554
115477
|
let d2 = this.delegations.get(key);
|
|
114555
115478
|
let releaseObservation;
|
|
114556
115479
|
if (!d2 && this.autoBoundaryOpen(callerId)) {
|
|
@@ -114572,7 +115495,7 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114572
115495
|
return d2 ? await this.awaitAgent(key, d2, signal, observing) : this.agentAtRest(child);
|
|
114573
115496
|
case "stop": {
|
|
114574
115497
|
if (!d2 || d2.settled) return { ok: false, error: `Nothing you gave "${child.name}" is running.` };
|
|
114575
|
-
if (!observing && this.awaitingReport.get(child.id) === callerId) this.
|
|
115498
|
+
if (!observing && this.awaitingReport.get(child.id) === callerId) this.clearReport(child.id);
|
|
114576
115499
|
d2.stop("the session that started it stopped it", observing);
|
|
114577
115500
|
await d2.outcome;
|
|
114578
115501
|
if (!observing) this.delegations.delete(key);
|
|
@@ -114607,7 +115530,12 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114607
115530
|
if (!this.adapters.get(child.tool)?.capabilities.midTurnInput || !this.handles.get(child.id)?.sendTracked) {
|
|
114608
115531
|
return { ok: false, error: `"${child.name}" cannot take messages while working. Wait for it (action "wait") or stop it first.` };
|
|
114609
115532
|
}
|
|
114610
|
-
const
|
|
115533
|
+
const own = observing ? this.observeTurn(child, true, true) : void 0;
|
|
115534
|
+
if (own) void own.run.wait.done.then(own.release, own.release);
|
|
115535
|
+
const record2 = own ? { run: own.run, waiters: 0, told: false } : void 0;
|
|
115536
|
+
if (record2) this.observerNotes.set(key, record2);
|
|
115537
|
+
const wait = own?.run.wait ?? d2.wait;
|
|
115538
|
+
const releaseHold = wait.holdInput();
|
|
114611
115539
|
const release = () => {
|
|
114612
115540
|
releaseHold();
|
|
114613
115541
|
this.applyDeferredRestart(child.id);
|
|
@@ -114615,12 +115543,24 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114615
115543
|
try {
|
|
114616
115544
|
await this.deliver(child.id, text2, void 0, from, false, void 0, "inline", (h, input) => {
|
|
114617
115545
|
const fail4 = (e) => {
|
|
114618
|
-
|
|
115546
|
+
if (!record2) {
|
|
115547
|
+
wait.fail(e);
|
|
115548
|
+
return release();
|
|
115549
|
+
}
|
|
115550
|
+
wait.fail(new Error(`your note was not delivered: ${e.message}`));
|
|
114619
115551
|
release();
|
|
115552
|
+
if (!record2.told && !record2.waiters && this.observerNotes.get(key) === record2) {
|
|
115553
|
+
record2.told = true;
|
|
115554
|
+
void this.tellNoteRefused(callerId, child.id, e.message);
|
|
115555
|
+
}
|
|
114620
115556
|
};
|
|
114621
115557
|
void h.sendTracked(input, { complete: release, fail: fail4 }).then(release, fail4);
|
|
114622
115558
|
});
|
|
114623
115559
|
} catch (e) {
|
|
115560
|
+
if (record2) {
|
|
115561
|
+
wait.fail(e);
|
|
115562
|
+
if (this.observerNotes.get(key) === record2) this.observerNotes.delete(key);
|
|
115563
|
+
}
|
|
114624
115564
|
release();
|
|
114625
115565
|
return { ok: false, error: `Could not send it: ${e.message}` };
|
|
114626
115566
|
}
|
|
@@ -114651,6 +115591,44 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114651
115591
|
releaseObservation?.();
|
|
114652
115592
|
}
|
|
114653
115593
|
}
|
|
115594
|
+
/**
|
|
115595
|
+
* agent_session on a start_agent request's handle (#673). While the card stands: status says so, wait waits for the
|
|
115596
|
+
* person's answer up to the bound, stop (or delete) withdraws the card, send is refused. Once the child started, the
|
|
115597
|
+
* call goes on to that session (a wait that saw it start answers its status, so the call stays inside its bound);
|
|
115598
|
+
* once it ended without one, the reason.
|
|
115599
|
+
*/
|
|
115600
|
+
async agentStartSession(rec, action, signal) {
|
|
115601
|
+
const id2 = rec.req.requestId;
|
|
115602
|
+
if (!rec.ended) {
|
|
115603
|
+
if (action === "stop" || action === "delete") {
|
|
115604
|
+
this.withdrawAgentStart(id2, "The session that asked withdrew the request.", agentStartPersonText(rec.req, "stopped"));
|
|
115605
|
+
return { ok: true, state: "stopped", sessionId: id2, sessionName: "", tool: rec.req.detail.tool };
|
|
115606
|
+
}
|
|
115607
|
+
if (action === "send") return { ok: false, error: `No session yet: request ${id2} is still waiting for the person's answer. Send once it has started.` };
|
|
115608
|
+
if (action === "wait") {
|
|
115609
|
+
let timer;
|
|
115610
|
+
let onAbort;
|
|
115611
|
+
await Promise.race([
|
|
115612
|
+
rec.done,
|
|
115613
|
+
new Promise((resolve12) => timer = setTimeout(resolve12, ASK_WAIT_MS)),
|
|
115614
|
+
new Promise((resolve12) => {
|
|
115615
|
+
if (!signal) return;
|
|
115616
|
+
onAbort = resolve12;
|
|
115617
|
+
if (signal.aborted) resolve12();
|
|
115618
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
115619
|
+
})
|
|
115620
|
+
]);
|
|
115621
|
+
clearTimeout(timer);
|
|
115622
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
115623
|
+
const ended2 = rec.ended;
|
|
115624
|
+
if (ended2 && "childId" in ended2) return { childId: ended2.childId, action: "status" };
|
|
115625
|
+
}
|
|
115626
|
+
if (!rec.ended) return agentStartPending(rec.req);
|
|
115627
|
+
}
|
|
115628
|
+
const ended = rec.ended;
|
|
115629
|
+
if ("error" in ended) return { ok: false, error: `Request ${id2} started no session: ${ended.error}` };
|
|
115630
|
+
return { childId: ended.childId, action };
|
|
115631
|
+
}
|
|
114654
115632
|
/**
|
|
114655
115633
|
* The branch a child's new worktree goes on (#561 part 2): the one asked for, checked by git's own rule, or one
|
|
114656
115634
|
* named from the job (`agentBranch`). Refused before the card when the project is no git repository, so the person
|
|
@@ -114663,8 +115641,22 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114663
115641
|
if (asked) return await gitValidBranchName(asked) ? { branch: asked } : { error: `Not a valid branch name: ${asked}` };
|
|
114664
115642
|
return { branch: agentBranch(opts.task, randomUUID10()) };
|
|
114665
115643
|
}
|
|
114666
|
-
/** Prevent a send/resume from racing the snapshot and worktree removal
|
|
114667
|
-
worktreeCleanup = /* @__PURE__ */ new
|
|
115644
|
+
/** Prevent a send/resume from racing the snapshot and worktree removal: the child, by id, and its worktree folder */
|
|
115645
|
+
worktreeCleanup = /* @__PURE__ */ new Map();
|
|
115646
|
+
/**
|
|
115647
|
+
* Whether a cleanup under way holds this session still (#647): the child whose worktree goes, and every session whose
|
|
115648
|
+
* folder is inside that worktree. A helper the child started works there; woken during the snapshot, it would write
|
|
115649
|
+
* into a folder about to be removed. Cheap while no cleanup runs, which is nearly always.
|
|
115650
|
+
*/
|
|
115651
|
+
worktreeCleanupBlocks(sessionId) {
|
|
115652
|
+
if (this.worktreeCleanup.size === 0) return false;
|
|
115653
|
+
if (this.worktreeCleanup.has(sessionId)) return true;
|
|
115654
|
+
const cwd = this.store.sessionCwd(sessionId) ?? this.meta.get(sessionId)?.worktree?.path;
|
|
115655
|
+
if (!cwd) return false;
|
|
115656
|
+
const path2 = canonicalPath(cwd);
|
|
115657
|
+
for (const folder of this.worktreeCleanup.values()) if (insideFolder(path2, folder)) return true;
|
|
115658
|
+
return false;
|
|
115659
|
+
}
|
|
114668
115660
|
/** #608: the parent decides; local work follows the session into the trash in every preset. */
|
|
114669
115661
|
async cleanUpWorktreeChild(target) {
|
|
114670
115662
|
if (!target.worktree?.base || !target.projectId) {
|
|
@@ -114678,8 +115670,16 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114678
115670
|
}
|
|
114679
115671
|
if (this.resuming.has(target.id) || this.deferredRestarts.has(target.id)) return { ok: false, error: `"${target.name}" is waking or restarting \u2014 wait for it first` };
|
|
114680
115672
|
if (this.worktreeCleanup.has(target.id)) return { ok: false, error: "This worktree is already being removed" };
|
|
114681
|
-
this.
|
|
115673
|
+
if (this.trashing.has(target.id)) return { ok: false, error: `"${target.name}" is already being deleted; its worktree stays in place` };
|
|
115674
|
+
const occupied = () => {
|
|
115675
|
+
const occupants = this.sessionsInside(target);
|
|
115676
|
+
if (!occupants.length) return null;
|
|
115677
|
+
return { ok: false, error: `${occupants.map((s) => `"${s.name}"`).join(", ")} ${occupants.length === 1 ? "is" : "are"} still in the worktree of "${target.name}" \u2014 delete ${occupants.length === 1 ? "it" : "them"} first` };
|
|
115678
|
+
};
|
|
115679
|
+
const taken = occupied();
|
|
115680
|
+
if (taken) return taken;
|
|
114682
115681
|
const { branch, path: path2 } = target.worktree;
|
|
115682
|
+
this.worktreeCleanup.set(target.id, canonicalPath(path2));
|
|
114683
115683
|
try {
|
|
114684
115684
|
const cwd = this.cwdOf(target.projectId);
|
|
114685
115685
|
const handle = this.handles.get(target.id);
|
|
@@ -114700,7 +115700,11 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114700
115700
|
text: `Local work saved before worktree deletion: ${setAside.changedFiles} uncommitted files, ${setAside.commits} commits not on the trunk. Recovery patches and bundles are at ${worktreeRecoveryDir(target.id)}. Restore from Settings \u2192 Trash or recover these files manually before deleting this session for good.`
|
|
114701
115701
|
});
|
|
114702
115702
|
}
|
|
115703
|
+
const appeared = occupied();
|
|
115704
|
+
if (appeared) return appeared;
|
|
114703
115705
|
await this.trashSession(target.id, false, false, setAside);
|
|
115706
|
+
const appearedSince = occupied();
|
|
115707
|
+
if (appearedSince) return { ok: true, branch, setAside, warning: `The session is in the trash, but worktree ${path2} and branch ${branch} were left: ${appearedSince.error}. Recovery files are at ${worktreeRecoveryDir(target.id)}.` };
|
|
114704
115708
|
try {
|
|
114705
115709
|
await gitWorktreeRemove(cwd, path2, true, true);
|
|
114706
115710
|
} catch (e) {
|
|
@@ -114708,6 +115712,8 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114708
115712
|
}
|
|
114709
115713
|
try {
|
|
114710
115714
|
await gitBranchDelete(cwd, branch, this.gitTrust(target.projectId));
|
|
115715
|
+
await clearRecoveredBranchMark(cwd, branch).catch(() => {
|
|
115716
|
+
});
|
|
114711
115717
|
} catch (e) {
|
|
114712
115718
|
return { ok: true, branch, setAside, warning: `The session is in the trash and its worktree was removed, but branch ${branch} was left: ${e.message}. Recovery files are at ${worktreeRecoveryDir(target.id)}.` };
|
|
114713
115719
|
}
|
|
@@ -114718,12 +115724,27 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114718
115724
|
this.worktreeCleanup.delete(target.id);
|
|
114719
115725
|
}
|
|
114720
115726
|
}
|
|
115727
|
+
/**
|
|
115728
|
+
* The other sessions whose folder is inside this worktree (#647): a helper the child started without a worktree of
|
|
115729
|
+
* its own works in the child's. One asleep counts too — after a host swap no session has a process, and a message
|
|
115730
|
+
* wakes it right there. Removing the worktree under it would lose what it writes next, so cleanup is refused.
|
|
115731
|
+
*/
|
|
115732
|
+
sessionsInside(target) {
|
|
115733
|
+
const root3 = target.worktree?.path;
|
|
115734
|
+
if (!root3) return [];
|
|
115735
|
+
const folder = canonicalPath(root3);
|
|
115736
|
+
return [...this.meta.values()].filter((s) => {
|
|
115737
|
+
if (s.id === target.id) return false;
|
|
115738
|
+
const cwd = this.store.sessionCwd(s.id) ?? s.worktree?.path;
|
|
115739
|
+
return !!cwd && insideFolder(canonicalPath(cwd), folder);
|
|
115740
|
+
});
|
|
115741
|
+
}
|
|
114721
115742
|
/** A saved ceiling overrides the caller's preset default: auto unlimited, otherwise AGENT_CEILING. */
|
|
114722
115743
|
agentCeilingError(callerId, projectId) {
|
|
114723
115744
|
const configured = this.uiPreferences().agentCeiling;
|
|
114724
115745
|
const ceiling = configured ?? (this.autoBoundaryOpen(callerId) ? 0 : AGENT_CEILING);
|
|
114725
115746
|
if (ceiling === 0) return null;
|
|
114726
|
-
const running = [...this.delegations.values()].filter((d2) => d2.agent && !d2.settled && this.meta.get(d2.sessionId)?.projectId === projectId).length;
|
|
115747
|
+
const running = [...this.delegations.values()].filter((d2) => d2.agent && !d2.settled && this.meta.get(d2.sessionId)?.projectId === projectId).length + (this.agentStartReserved.get(projectId) ?? 0);
|
|
114727
115748
|
if (running < ceiling) return null;
|
|
114728
115749
|
return `${running} agents started by this project's sessions are already working, the most at once. Wait for one (agent_session, action "wait") or stop one, then try again.`;
|
|
114729
115750
|
}
|
|
@@ -114733,22 +115754,38 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114733
115754
|
* list's ids or labels contain (an alias such as "sonnet") passes, and a tool that cannot list its models passes all.
|
|
114734
115755
|
*/
|
|
114735
115756
|
async agentChoiceError(tool, askedModel, askedEffort, model) {
|
|
114736
|
-
if (!askedModel && !askedEffort) return null;
|
|
114737
|
-
const
|
|
114738
|
-
if (!
|
|
114739
|
-
const
|
|
114740
|
-
|
|
114741
|
-
|
|
114742
|
-
|
|
114743
|
-
|
|
114744
|
-
|
|
114745
|
-
|
|
114746
|
-
if (
|
|
115757
|
+
if (!askedModel && !askedEffort) return { error: null };
|
|
115758
|
+
const adapter = this.adapters.get(tool);
|
|
115759
|
+
if (!adapter?.listModels) return { error: null };
|
|
115760
|
+
const what = [askedModel && `model "${askedModel}"`, askedEffort && `effort "${askedEffort}"`].filter(Boolean).join(" and ");
|
|
115761
|
+
const notChecked = (why) => ({ error: null, unchecked: `${what} not checked: ${why}. If the session's first turn fails on it, start again without it` });
|
|
115762
|
+
const mismatch = (models) => {
|
|
115763
|
+
const find = (name) => {
|
|
115764
|
+
const n = name.toLowerCase();
|
|
115765
|
+
return models.find((m) => m.id.toLowerCase() === n) ?? models.find((m) => m.id.toLowerCase().includes(n) || m.label.toLowerCase().includes(n));
|
|
115766
|
+
};
|
|
115767
|
+
if (askedModel && !find(askedModel)) return `${tool} has no model "${askedModel}". Its models: ${models.map((m) => m.id).join(", ")}.`;
|
|
115768
|
+
if (askedEffort) {
|
|
115769
|
+
const m = model ? find(model) : models[0];
|
|
115770
|
+
if (m && m.efforts.length > 0 && !m.efforts.includes(askedEffort)) return `${m.id} has no effort "${askedEffort}". Its efforts: ${m.efforts.join(", ")}.`;
|
|
115771
|
+
}
|
|
115772
|
+
return null;
|
|
115773
|
+
};
|
|
115774
|
+
let got = await this.modelsWithin(tool, START_AGENT_MODEL_CHECK_MS);
|
|
115775
|
+
if (!got) return notChecked(`${tool}'s model list was not available in time`);
|
|
115776
|
+
if (got.models.length === 0) return notChecked(`${tool} listed no models`);
|
|
115777
|
+
let wrong = mismatch(got.models);
|
|
115778
|
+
if (wrong && got.cached) {
|
|
115779
|
+
got = await this.modelsWithin(tool, START_AGENT_MODEL_CHECK_MS, true);
|
|
115780
|
+
if (!got) return notChecked(`${tool}'s saved model list lacks it and a new one was not available in time`);
|
|
115781
|
+
if (got.models.length === 0) return notChecked(`${tool} listed no models`);
|
|
115782
|
+
wrong = mismatch(got.models);
|
|
114747
115783
|
}
|
|
114748
|
-
return
|
|
115784
|
+
return { error: wrong };
|
|
114749
115785
|
}
|
|
114750
115786
|
/** Update grants last only for the calling session on this host; they never change automatic application. */
|
|
114751
|
-
async updateCentraluAsked(sessionId, action) {
|
|
115787
|
+
async updateCentraluAsked(sessionId, action, signal) {
|
|
115788
|
+
if (action === "apply") return this.applyUpdateAsked(sessionId, signal);
|
|
114752
115789
|
const updates2 = this.updates;
|
|
114753
115790
|
if (!updates2) return { ok: false, error: "Update checks are unavailable" };
|
|
114754
115791
|
if (this.toolProfileOf(sessionId) === "reader" && !this.updateConsents.has(sessionId)) {
|
|
@@ -114756,19 +115793,71 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114756
115793
|
if (decision === "always") this.updateConsents.add(sessionId);
|
|
114757
115794
|
else if (decision !== "allow") return { ok: false, error: "The person did not allow this Centralu update. Do not ask again unless they tell you to." };
|
|
114758
115795
|
}
|
|
114759
|
-
return { ok: true, status: action === "check" ? await updates2.check(true) : updates2.apply() };
|
|
115796
|
+
return { ok: true, status: action === "check" ? await updates2.check(true) : await updates2.apply() };
|
|
115797
|
+
}
|
|
115798
|
+
/**
|
|
115799
|
+
* `update_centralu` action "apply" (#665): a window relaunches into the installed build, as its "Apply now" does.
|
|
115800
|
+
*
|
|
115801
|
+
* By the caller's own preset, read now (as every boundary of #606): `auto` applies at once, `normal` raises a card in
|
|
115802
|
+
* the caller (after a window said it is ready, so "nothing installed" never asks), `safe` is refused, as is an agent an
|
|
115803
|
+
* app started. "Always" on the card covers applying for this session only, apart from check and install. The window
|
|
115804
|
+
* still makes every check its button makes (update-apply.ts), so an agent can only ask for what the person could click.
|
|
115805
|
+
*/
|
|
115806
|
+
async applyUpdateAsked(sessionId, signal) {
|
|
115807
|
+
const broker = this.updateApply;
|
|
115808
|
+
if (!broker) return { ok: false, error: "Applying updates is unavailable on this host" };
|
|
115809
|
+
const m = this.meta.get(sessionId);
|
|
115810
|
+
if (!m) return { ok: false, error: "The calling session is gone" };
|
|
115811
|
+
if (this.isAppAgentSession(m)) return { ok: false, error: "An agent an app started cannot apply Centralu updates." };
|
|
115812
|
+
if (m.permissionPreset === "safe") {
|
|
115813
|
+
return {
|
|
115814
|
+
ok: false,
|
|
115815
|
+
error: "This session's preset is safe, which cannot relaunch Centralu. Tell the person the update is ready to apply from the update line; do not ask again unless they tell you to."
|
|
115816
|
+
};
|
|
115817
|
+
}
|
|
115818
|
+
const outcome = await broker.apply(async () => {
|
|
115819
|
+
if (this.updateApplyConsents.has(sessionId)) return null;
|
|
115820
|
+
const decision = await this.askHost(sessionId, { kind: "other", raw: CENTRALU_APPLY_QUESTION, about: CENTRALU_APPLY }, signal);
|
|
115821
|
+
if (decision === "always") this.updateApplyConsents.add(sessionId);
|
|
115822
|
+
else if (decision === null) return "The question to the person was withdrawn before they answered, so nothing was relaunched.";
|
|
115823
|
+
else if (decision !== "allow") return "The person did not allow this session to apply the Centralu update. Do not ask again unless they tell you to.";
|
|
115824
|
+
return null;
|
|
115825
|
+
});
|
|
115826
|
+
if (!outcome.ok) return { ok: false, error: outcome.error };
|
|
115827
|
+
console.error(`[updates] session ${sessionId} applied the installed update: a window is relaunching${outcome.version ? ` into ${outcome.version}` : ""}`);
|
|
115828
|
+
return { ok: true, relaunched: outcome.version ? { version: outcome.version } : {} };
|
|
114760
115829
|
}
|
|
114761
115830
|
/**
|
|
114762
115831
|
* The person's consent for a session to start agents (#561), the first time it does: a card in the caller's approval
|
|
114763
115832
|
* slot. "Always" is kept for this session while this host serves it; with ⌥ (scope `project`) it is remembered as the
|
|
114764
115833
|
* project's 'agents' consent, listed in Settings → Permissions with ask_project's pairs and revoked there.
|
|
115834
|
+
*
|
|
115835
|
+
* **Trust is the consent (#673).** A `normal` caller in a trusted project starts agents without a card: the person
|
|
115836
|
+
* already decided about that project when they trusted it (#655), and a card in a session they were not looking at
|
|
115837
|
+
* only made the session work alone. `safe` keeps the card, as it keeps every boundary closed (#606); `auto` opens it.
|
|
115838
|
+
* `onCard` hears the request id before a card is raised, so the request can be kept until the person answers.
|
|
114765
115839
|
*/
|
|
114766
|
-
async ensureAgentConsent(callerId, detail, signal) {
|
|
115840
|
+
async ensureAgentConsent(callerId, detail, signal, onCard) {
|
|
114767
115841
|
if (this.agentConsents.has(callerId)) return { ok: true };
|
|
114768
115842
|
if (this.store.getProjectConsent(detail.project.id, detail.project.id, "agents")) return { ok: true };
|
|
114769
|
-
|
|
115843
|
+
if (this.trustIsAgentConsent(callerId, detail.project.id)) return { ok: true };
|
|
115844
|
+
const requestId = `ag-${randomUUID10()}`;
|
|
115845
|
+
if (this.meta.get(callerId)?.permissionPreset !== "auto") onCard?.(requestId);
|
|
115846
|
+
return this.agentConsentFrom(callerId, detail.project.id, await this.askHost(callerId, detail, signal, { requestId }));
|
|
115847
|
+
}
|
|
115848
|
+
/** Depth one (#561): a session started by another, or handling an ask_project task now, starts no agents */
|
|
115849
|
+
agentDepthBlocked(caller) {
|
|
115850
|
+
return !!caller.askedBy || [...this.delegations.values()].some((d2) => d2.sessionId === caller.id && !d2.agent && !d2.settled);
|
|
115851
|
+
}
|
|
115852
|
+
/** A `normal` caller in a trusted project needs no card to start agents (#673): the project's trust, read now */
|
|
115853
|
+
trustIsAgentConsent(callerId, projectId) {
|
|
115854
|
+
if (this.meta.get(callerId)?.permissionPreset !== "normal") return false;
|
|
115855
|
+
return this.store.listProjects().find((p) => p.id === projectId)?.trusted === true;
|
|
115856
|
+
}
|
|
115857
|
+
/** The person's answer on a start_agent card, as consent (remembered where they chose "always") or a refusal */
|
|
115858
|
+
agentConsentFrom(callerId, projectId, decision) {
|
|
114770
115859
|
if (decision === "always_project") {
|
|
114771
|
-
this.store.setProjectConsent(
|
|
115860
|
+
this.store.setProjectConsent(projectId, projectId, "agents");
|
|
114772
115861
|
this.emit({ type: "project_consents_changed" });
|
|
114773
115862
|
return { ok: true };
|
|
114774
115863
|
}
|
|
@@ -114777,15 +115866,112 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114777
115866
|
return { ok: true };
|
|
114778
115867
|
}
|
|
114779
115868
|
if (decision === "allow") return { ok: true };
|
|
114780
|
-
if (decision === "deny")
|
|
114781
|
-
|
|
115869
|
+
if (decision === "deny") {
|
|
115870
|
+
return { ok: false, why: "denied", error: `The person did not allow this session to start another agent. Do not start one again unless they tell you to. ${ASK_THE_PERSON}` };
|
|
115871
|
+
}
|
|
115872
|
+
return { ok: false, why: "withdrawn", error: `The question to the person was withdrawn before they answered, so no agent was started. ${ASK_THE_PERSON}` };
|
|
115873
|
+
}
|
|
115874
|
+
/** A start_agent request whose card now stands (#673): kept, in memory and in the store, until it is answered */
|
|
115875
|
+
openAgentStart(req) {
|
|
115876
|
+
let end;
|
|
115877
|
+
const done = new Promise((resolve12) => end = resolve12);
|
|
115878
|
+
const rec = { req, detached: false, done, end };
|
|
115879
|
+
this.agentStarts.set(req.requestId, rec);
|
|
115880
|
+
this.store.putAgentStartRequest(req.requestId, req.callerId, req);
|
|
115881
|
+
return rec;
|
|
115882
|
+
}
|
|
115883
|
+
/**
|
|
115884
|
+
* How a start_agent request ended (#673), once: the row goes, a wait on its handle returns, and unless a session
|
|
115885
|
+
* started, the person is shown why in the caller's session (not only the model, in a tool result). A request whose
|
|
115886
|
+
* call already answered "pending" is told to the caller as a message, since no call is left to answer it.
|
|
115887
|
+
* A refusal the person gave on the card while the call waited needs no line: the card itself says "Denied".
|
|
115888
|
+
*/
|
|
115889
|
+
endAgentStart(requestId, ended) {
|
|
115890
|
+
const rec = requestId ? this.agentStarts.get(requestId) : void 0;
|
|
115891
|
+
if (!rec || rec.ended) return;
|
|
115892
|
+
rec.ended = "childId" in ended ? { childId: ended.childId } : { error: ended.error };
|
|
115893
|
+
rec.end();
|
|
115894
|
+
this.store.deleteAgentStartRequest(rec.req.requestId);
|
|
115895
|
+
if ("childId" in ended) return;
|
|
115896
|
+
const { callerId } = rec.req;
|
|
115897
|
+
if (!this.meta.has(callerId)) return;
|
|
115898
|
+
if (!ended.denied || rec.detached) {
|
|
115899
|
+
this.onEvent({ type: "notice", sessionId: callerId, level: "warning", from: "Centralu", label: "agent request", audience: "you", text: ended.person });
|
|
115900
|
+
}
|
|
115901
|
+
if (rec.detached) void this.tellAgentStartEnded(rec.req, ended.error);
|
|
115902
|
+
}
|
|
115903
|
+
/** The caller of a request that already answered "pending" hears how it ended (#673), as a message in its session */
|
|
115904
|
+
async tellAgentStartEnded(req, why) {
|
|
115905
|
+
if (this.handingOver || this.disposing || !this.meta.has(req.callerId)) return;
|
|
115906
|
+
try {
|
|
115907
|
+
await this.deliver(
|
|
115908
|
+
req.callerId,
|
|
115909
|
+
`[Centralu] Your start_agent request ${req.requestId} did not start a session.
|
|
115910
|
+
Agent: ${frameField(req.detail.tool)}
|
|
115911
|
+
Task: ${frameField(req.detail.text)}
|
|
115912
|
+
|
|
115913
|
+
Reason: ${why.slice(0, 600)}`,
|
|
115914
|
+
void 0,
|
|
115915
|
+
{ sessionId: HOST_SENDER, name: "Centralu" },
|
|
115916
|
+
false
|
|
115917
|
+
);
|
|
115918
|
+
} catch {
|
|
115919
|
+
}
|
|
115920
|
+
}
|
|
115921
|
+
/**
|
|
115922
|
+
* Withdraws a request's card, when the session that asked withdraws it or is deleted (#673): the card closes, the
|
|
115923
|
+
* request ends with `person` shown, and whatever waits on the card hears no answer.
|
|
115924
|
+
*/
|
|
115925
|
+
withdrawAgentStart(requestId, error51, person) {
|
|
115926
|
+
const rec = this.agentStarts.get(requestId);
|
|
115927
|
+
if (!rec || rec.ended) return;
|
|
115928
|
+
this.endAgentStart(requestId, { error: error51, person });
|
|
115929
|
+
const ask = this.hostAsks.get(requestId);
|
|
115930
|
+
if (!ask) return;
|
|
115931
|
+
this.hostAsks.delete(requestId);
|
|
115932
|
+
if (this.meta.get(ask.sessionId)?.pendingApproval?.requestId === requestId) {
|
|
115933
|
+
this.restAfterCard(ask);
|
|
115934
|
+
this.onEvent({ type: "approval_resolved", sessionId: ask.sessionId, requestId, decision: "deny" });
|
|
115935
|
+
}
|
|
115936
|
+
ask.resolve(null);
|
|
115937
|
+
}
|
|
115938
|
+
/**
|
|
115939
|
+
* A request the previous host left standing (#673, `agent_start_requests`): its card is raised again, and an
|
|
115940
|
+
* approval starts the child here, with the report armed, as the call would have. A request whose caller is gone ends.
|
|
115941
|
+
*/
|
|
115942
|
+
resumeAgentStart(req) {
|
|
115943
|
+
const caller = this.meta.get(req.callerId);
|
|
115944
|
+
if (!caller) {
|
|
115945
|
+
this.store.deleteAgentStartRequest(req.requestId);
|
|
115946
|
+
return;
|
|
115947
|
+
}
|
|
115948
|
+
const rec = this.openAgentStart(req);
|
|
115949
|
+
rec.detached = true;
|
|
115950
|
+
void (async () => {
|
|
115951
|
+
const decision = await this.askHost(req.callerId, req.detail, void 0, { requestId: req.requestId, shown: true });
|
|
115952
|
+
if (decision === null && (this.handingOver || this.disposing)) return;
|
|
115953
|
+
const consent = this.agentConsentFrom(req.callerId, req.projectId, decision);
|
|
115954
|
+
if (!consent.ok) {
|
|
115955
|
+
this.endAgentStart(req.requestId, { error: consent.error, person: agentStartPersonText(req, consent.why === "denied" ? "denied" : "withdrawn"), denied: consent.why === "denied" });
|
|
115956
|
+
return;
|
|
115957
|
+
}
|
|
115958
|
+
const project2 = this.store.listProjects().find((p) => p.id === req.projectId);
|
|
115959
|
+
const r = project2 ? await this.createAgentChild(req.callerId, { id: project2.id, name: project2.name, path: project2.path }, req, { stage: "creating" }, req.detail.preset) : { done: { ok: false, error: "This session's project is no longer registered." } };
|
|
115960
|
+
if ("key" in r) {
|
|
115961
|
+
this.endAgentStart(req.requestId, { childId: r.d.sessionId });
|
|
115962
|
+
this.agentWorking(r.d);
|
|
115963
|
+
} else {
|
|
115964
|
+
const error51 = r.done.ok ? "No session was created." : r.done.error;
|
|
115965
|
+
this.endAgentStart(req.requestId, { error: error51, person: agentStartPersonText(req, "failed", error51) });
|
|
115966
|
+
}
|
|
115967
|
+
})();
|
|
114782
115968
|
}
|
|
114783
115969
|
/**
|
|
114784
115970
|
* "Working": the caller is told when the turn ends (#561), through the report the orchestrator's reportBack uses, unless
|
|
114785
115971
|
* a call is waiting on it by then (`awaitAgent` takes the marker down while it waits)
|
|
114786
115972
|
*/
|
|
114787
115973
|
agentWorking(d2, arm = true, observing = false) {
|
|
114788
|
-
if (arm && !observing) this.
|
|
115974
|
+
if (arm && !observing) this.markReport(d2.sessionId, d2.callerId);
|
|
114789
115975
|
const notice = d2.notice();
|
|
114790
115976
|
return {
|
|
114791
115977
|
ok: true,
|
|
@@ -114797,6 +115983,43 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114797
115983
|
...notice ? { notice } : {}
|
|
114798
115984
|
};
|
|
114799
115985
|
}
|
|
115986
|
+
/** An observer's `status` / `wait` on the note it sent (#646): its own reader's outcome, taken once it has settled */
|
|
115987
|
+
async readObserverNote(key, note, wait, signal) {
|
|
115988
|
+
if (!wait && !note.run.settled) return this.agentWorking(note.run, false, true);
|
|
115989
|
+
note.waiters++;
|
|
115990
|
+
let r;
|
|
115991
|
+
try {
|
|
115992
|
+
r = await this.awaitAgent(key, note.run, signal, true);
|
|
115993
|
+
} finally {
|
|
115994
|
+
note.waiters--;
|
|
115995
|
+
}
|
|
115996
|
+
if (note.run.settled) {
|
|
115997
|
+
note.told = true;
|
|
115998
|
+
if (this.observerNotes.get(key) === note) this.observerNotes.delete(key);
|
|
115999
|
+
}
|
|
116000
|
+
return r;
|
|
116001
|
+
}
|
|
116002
|
+
/** The observer's note was refused after its send returned (#646): said in the observer's session, as a report is */
|
|
116003
|
+
async tellNoteRefused(observerId, sessionId, why) {
|
|
116004
|
+
if (this.handingOver || !this.meta.has(observerId)) return;
|
|
116005
|
+
const name = this.meta.get(sessionId)?.name ?? sessionId;
|
|
116006
|
+
try {
|
|
116007
|
+
await this.deliver(
|
|
116008
|
+
observerId,
|
|
116009
|
+
`[Centralu] Your note to another session was not delivered.
|
|
116010
|
+
Session: ${frameField(name)}
|
|
116011
|
+
id: ${sessionId}
|
|
116012
|
+
|
|
116013
|
+
Reason: ${why.slice(0, 600)}
|
|
116014
|
+
|
|
116015
|
+
Send it again with agent_session, or read that session's recent conversation with read_session.`,
|
|
116016
|
+
void 0,
|
|
116017
|
+
{ sessionId, name },
|
|
116018
|
+
true
|
|
116019
|
+
);
|
|
116020
|
+
} catch {
|
|
116021
|
+
}
|
|
116022
|
+
}
|
|
114800
116023
|
/** A child with nothing of the caller's running: its last final answer (#561) */
|
|
114801
116024
|
agentAtRest(child) {
|
|
114802
116025
|
return {
|
|
@@ -114810,12 +116033,12 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114810
116033
|
};
|
|
114811
116034
|
}
|
|
114812
116035
|
/** Waits on a child's turn up to the bound (#561), like `awaitDelegation`, and reads its outcome for the caller */
|
|
114813
|
-
async awaitAgent(key, d2, signal, observing = false) {
|
|
114814
|
-
if (!observing && this.awaitingReport.get(d2.sessionId) === d2.callerId) this.
|
|
116036
|
+
async awaitAgent(key, d2, signal, observing = false, boundMs = ASK_WAIT_MS) {
|
|
116037
|
+
if (!observing && this.awaitingReport.get(d2.sessionId) === d2.callerId) this.clearReport(d2.sessionId);
|
|
114815
116038
|
const name = () => this.meta.get(d2.sessionId)?.name ?? d2.sessionId;
|
|
114816
116039
|
let timer;
|
|
114817
116040
|
let onAbort;
|
|
114818
|
-
const bound = new Promise((resolve12) => timer = setTimeout(() => resolve12("bound"),
|
|
116041
|
+
const bound = new Promise((resolve12) => timer = setTimeout(() => resolve12("bound"), boundMs));
|
|
114819
116042
|
const aborted2 = new Promise((resolve12) => {
|
|
114820
116043
|
if (!signal) return;
|
|
114821
116044
|
onAbort = () => resolve12("aborted");
|
|
@@ -114918,6 +116141,7 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114918
116141
|
const next = [...this.hostAsks.values()].find((a) => a.sessionId === sessionId);
|
|
114919
116142
|
if (!next) return;
|
|
114920
116143
|
const e = { type: "approval_request", sessionId, requestId: next.requestId, detail: next.detail };
|
|
116144
|
+
next.restState = next.shown && !inTurn(m.state) ? m.state : void 0;
|
|
114921
116145
|
if (!next.shown) {
|
|
114922
116146
|
next.shown = true;
|
|
114923
116147
|
this.onEvent(e);
|
|
@@ -114927,6 +116151,19 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114927
116151
|
this.store.upsertSession(m);
|
|
114928
116152
|
this.emit(e);
|
|
114929
116153
|
}
|
|
116154
|
+
/**
|
|
116155
|
+
* A host card raised between turns closes back into that rest (#673), not into "working": no tool call is waiting on
|
|
116156
|
+
* it, so nothing would ever end that turn. Set before the card's `approval_resolved`, which then changes no state, and
|
|
116157
|
+
* without `onEvent`, since no turn ended (no report, no deferred restart).
|
|
116158
|
+
*/
|
|
116159
|
+
restAfterCard(ask) {
|
|
116160
|
+
const m = this.meta.get(ask.sessionId);
|
|
116161
|
+
if (!ask.restState || m?.pendingApproval?.requestId !== ask.requestId) return;
|
|
116162
|
+
const e = { type: "state_change", sessionId: ask.sessionId, state: ask.restState };
|
|
116163
|
+
this.applyStateHint(e, m);
|
|
116164
|
+
this.store.upsertSession(m);
|
|
116165
|
+
this.emit(e);
|
|
116166
|
+
}
|
|
114930
116167
|
/** Screen-originated capability questions still awaiting an answer (M4 D-4) */
|
|
114931
116168
|
appQuestionList() {
|
|
114932
116169
|
return [...this.appQuestions.values()].map((x) => ({ ...x.question })).sort((a, b2) => a.askedAt - b2.askedAt);
|
|
@@ -115169,8 +116406,7 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
115169
116406
|
if (!profileAllows(profile, name)) {
|
|
115170
116407
|
throw Object.assign(new Error(`Not a tool of this session: ${name}`), { code: "internal" });
|
|
115171
116408
|
}
|
|
115172
|
-
|
|
115173
|
-
return runOrchestratorTool(tools, name, args, { sessionId, profile });
|
|
116409
|
+
return runOrchestratorTool(this.toolBundleOf(sessionId, profile, m), name, args, { sessionId, profile });
|
|
115174
116410
|
}
|
|
115175
116411
|
/**
|
|
115176
116412
|
* Receives the external app runtime (M4 A-5) — a session that comes up after this receives whatever
|
|
@@ -115188,6 +116424,10 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
115188
116424
|
useUpdates(updates2) {
|
|
115189
116425
|
this.updates = updates2;
|
|
115190
116426
|
}
|
|
116427
|
+
/** The windows' "Apply now" for an agent (#665, update-apply.ts) — wired by the host, which owns the event stream */
|
|
116428
|
+
useUpdateApply(broker) {
|
|
116429
|
+
this.updateApply = broker;
|
|
116430
|
+
}
|
|
115191
116431
|
/**
|
|
115192
116432
|
* Says in the conversation that the session moved to a newer CLI (#297): restarting a process by itself is never
|
|
115193
116433
|
* silent. A notice from Centralu, not from the tool, so it reads as what happened rather than as something to fix.
|
|
@@ -115332,41 +116572,33 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
115332
116572
|
if (this.isAppAgentSession(m)) return void 0;
|
|
115333
116573
|
return this.appsHub?.attach({ id: m.id, kind: m.kind, projectId: m.projectId, builderOf: this.builderRefOf(m) }) ?? void 0;
|
|
115334
116574
|
}
|
|
116575
|
+
/** Is this an agent session stood up at an external app's request (M4 D-1; `isAppAgent` in tool-profiles.ts) */
|
|
116576
|
+
isAppAgentSession(m) {
|
|
116577
|
+
return isAppAgent({ kind: m.kind, appId: m.appId ?? null, builder: this.builderRefOf(m) !== null });
|
|
116578
|
+
}
|
|
115335
116579
|
/**
|
|
115336
|
-
*
|
|
115337
|
-
*
|
|
115338
|
-
*
|
|
115339
|
-
* reserved id belongs to the removed control app (#372), never to an external app.
|
|
116580
|
+
* What the profile rule (tool-profiles.ts) reads about this session: its row, whether its app's builder
|
|
116581
|
+
* slot points at it (looked up unless the caller just wrote it), and the Settings switch (#320) as it is
|
|
116582
|
+
* right now.
|
|
115340
116583
|
*/
|
|
115341
|
-
|
|
115342
|
-
return
|
|
116584
|
+
profileFacts(m, builder = this.builderRefOf(m) !== null) {
|
|
116585
|
+
return { kind: m.kind, projectId: m.projectId, appId: m.appId ?? null, builder, sessionTools: this.uiPreferences().sessionTools };
|
|
115343
116586
|
}
|
|
115344
116587
|
/**
|
|
115345
|
-
* The tool bundle this session receives (#69). null means no tools.
|
|
115346
|
-
*
|
|
115347
|
-
*
|
|
115348
|
-
* adoption must still be able to propose things after its children are cleaned up).
|
|
116588
|
+
* The tool bundle this session receives (#69). null means no tools. The rule is `toolProfileFor`
|
|
116589
|
+
* (tool-profiles.ts); creation and every wake hand the adapter the same answer, and the bridge asks
|
|
116590
|
+
* here on every call.
|
|
115349
116591
|
*/
|
|
115350
116592
|
toolProfileOf(sessionId) {
|
|
115351
116593
|
const m = this.meta.get(sessionId);
|
|
115352
|
-
|
|
115353
|
-
if (m.kind === "orchestrator") return "orchestrator";
|
|
115354
|
-
if (m.kind === "coordinator") return "scoped";
|
|
115355
|
-
if (this.builderRefOf(m)) return "builder";
|
|
115356
|
-
return this.readsOwnProject(m) ? "reader" : null;
|
|
116594
|
+
return m ? toolProfileFor(this.profileFacts(m)) : null;
|
|
115357
116595
|
}
|
|
115358
|
-
|
|
115359
|
-
|
|
115360
|
-
|
|
115361
|
-
|
|
115362
|
-
|
|
115363
|
-
|
|
115364
|
-
* The schemas are attached when the session starts, like every other bundle; turning the setting off
|
|
115365
|
-
* stops the calls at once (readerToolsFor and the bridge both ask again) and takes the schemas away
|
|
115366
|
-
* the next time the session starts.
|
|
115367
|
-
*/
|
|
115368
|
-
readsOwnProject(m) {
|
|
115369
|
-
return m.kind === "worker" && !!m.projectId && !this.isAppAgentSession(m) && this.uiPreferences().sessionTools;
|
|
116596
|
+
toolBundleOf(sessionId, profile, m) {
|
|
116597
|
+
return toolBundleFor(profile, m.scopeSessionIds, {
|
|
116598
|
+
orchestrator: () => this.orchestratorToolsFor(sessionId),
|
|
116599
|
+
scoped: (ids) => this.orchestratorToolsFor(sessionId, ids),
|
|
116600
|
+
reader: () => this.readerToolsFor(sessionId)
|
|
116601
|
+
});
|
|
115370
116602
|
}
|
|
115371
116603
|
/**
|
|
115372
116604
|
* A profile that directs other sessions — every profile but the reader. The reader set (#320)
|
|
@@ -115610,6 +116842,7 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
115610
116842
|
);
|
|
115611
116843
|
}
|
|
115612
116844
|
async disposeAll() {
|
|
116845
|
+
this.disposing = true;
|
|
115613
116846
|
this.watchers.close();
|
|
115614
116847
|
for (const readers of this.turnObservers.values()) for (const observer of readers) observer.fail(new Error("Centralu is stopping"));
|
|
115615
116848
|
this.appsHub?.rt.attachBrokerHost(null);
|
|
@@ -115641,6 +116874,7 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
115641
116874
|
async detachAll() {
|
|
115642
116875
|
this.watchers.close();
|
|
115643
116876
|
this.appsHub?.rt.attachBrokerHost(null);
|
|
116877
|
+
this.handingOver = true;
|
|
115644
116878
|
for (const run of [...this.agentRuns.values()]) run.fail(new Error("Centralu is restarting"));
|
|
115645
116879
|
for (const readers of this.turnObservers.values()) for (const observer of readers) observer.fail(new Error("Centralu is restarting"));
|
|
115646
116880
|
for (const ask of [...this.hostAsks.values()]) ask.resolve(null);
|
|
@@ -115674,7 +116908,7 @@ import { relative as relative9 } from "node:path";
|
|
|
115674
116908
|
// packages/agent-host/src/themes.ts
|
|
115675
116909
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
115676
116910
|
import { watch as watch2 } from "node:fs";
|
|
115677
|
-
import { mkdir as mkdir7, readdir as readdir9, readFile as readFile5, rename as
|
|
116911
|
+
import { mkdir as mkdir7, readdir as readdir9, readFile as readFile5, rename as rename5, stat as stat6, unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
|
|
115678
116912
|
import { basename as basename6, join as join30 } from "node:path";
|
|
115679
116913
|
var MAX_FILE_BYTES = 256 * 1024;
|
|
115680
116914
|
var SETTLE_MS = 100;
|
|
@@ -115788,7 +117022,7 @@ var ThemeFiles = class {
|
|
|
115788
117022
|
const tmp = `${path2}.${randomBytes4(6).toString("hex")}.tmp`;
|
|
115789
117023
|
try {
|
|
115790
117024
|
await writeFile5(tmp, text2, "utf8");
|
|
115791
|
-
await
|
|
117025
|
+
await rename5(tmp, path2);
|
|
115792
117026
|
} catch (e) {
|
|
115793
117027
|
await unlink2(tmp).catch(() => {
|
|
115794
117028
|
});
|
|
@@ -115799,7 +117033,7 @@ var ThemeFiles = class {
|
|
|
115799
117033
|
|
|
115800
117034
|
// packages/agent-host/src/dev-services/strays.ts
|
|
115801
117035
|
import { execFile as execFile7 } from "node:child_process";
|
|
115802
|
-
import { realpathSync as
|
|
117036
|
+
import { realpathSync as realpathSync9 } from "node:fs";
|
|
115803
117037
|
import { promisify as promisify7 } from "node:util";
|
|
115804
117038
|
import { userInfo } from "node:os";
|
|
115805
117039
|
var exec7 = promisify7(execFile7);
|
|
@@ -115910,7 +117144,7 @@ function resolveRoots(roots) {
|
|
|
115910
117144
|
for (const r of roots) {
|
|
115911
117145
|
out.add(r);
|
|
115912
117146
|
try {
|
|
115913
|
-
out.add(
|
|
117147
|
+
out.add(realpathSync9(r));
|
|
115914
117148
|
} catch {
|
|
115915
117149
|
}
|
|
115916
117150
|
}
|
|
@@ -116023,7 +117257,7 @@ var toInfo = (h) => ({
|
|
|
116023
117257
|
function sessionIdsOf(panels) {
|
|
116024
117258
|
return panels.flatMap((p) => p.kind === "session" ? [p.sessionId] : []);
|
|
116025
117259
|
}
|
|
116026
|
-
function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: updates2, commands, externalApps: externalApps2, views: views2, inlineViews: inlineViews2, heldPids, themes: themes2, agentVersions: agentVersions2, sleeper: sleeper2, machines, stopHost, activity: activity2 } = {}) {
|
|
117260
|
+
function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: updates2, commands, externalApps: externalApps2, views: views2, inlineViews: inlineViews2, heldPids, themes: themes2, agentVersions: agentVersions2, sleeper: sleeper2, machines, stopHost, activity: activity2, updateApply: updateApply2 } = {}) {
|
|
116027
117261
|
const requireThemes = () => {
|
|
116028
117262
|
if (!themes2) throw Object.assign(new Error("Theme files are unavailable"), { code: "internal" });
|
|
116029
117263
|
return themes2;
|
|
@@ -116136,8 +117370,8 @@ function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: upd
|
|
|
116136
117370
|
"agents.resumeSession": async (p) => mgr2.resumeSession(RpcMethods["agents.resumeSession"].params.parse(p).sessionId),
|
|
116137
117371
|
"agents.forkConversation": async (p) => mgr2.forkConversation(RpcMethods["agents.forkConversation"].params.parse(p).sessionId),
|
|
116138
117372
|
"agents.updateSettings": async (p) => {
|
|
116139
|
-
const { sessionId, ...settings } = RpcMethods["agents.updateSettings"].params.parse(p);
|
|
116140
|
-
return await mgr2.updateSettings(sessionId, settings);
|
|
117373
|
+
const { sessionId, by, ...settings } = RpcMethods["agents.updateSettings"].params.parse(p);
|
|
117374
|
+
return await mgr2.updateSettings(sessionId, settings, by ?? "person");
|
|
116141
117375
|
},
|
|
116142
117376
|
"agents.switchTool": async (p) => {
|
|
116143
117377
|
const { sessionId, tool } = RpcMethods["agents.switchTool"].params.parse(p);
|
|
@@ -116497,7 +117731,7 @@ function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: upd
|
|
|
116497
117731
|
},
|
|
116498
117732
|
"projects.setTrusted": async (p) => {
|
|
116499
117733
|
const { projectId, trusted } = RpcMethods["projects.setTrusted"].params.parse(p);
|
|
116500
|
-
mgr2.setProjectTrusted(projectId, trusted);
|
|
117734
|
+
void mgr2.setProjectTrusted(projectId, trusted).catch((err) => console.error("[sessions] restarting for a trust change failed:", err));
|
|
116501
117735
|
externalApps2?.refresh();
|
|
116502
117736
|
return { ok: true };
|
|
116503
117737
|
},
|
|
@@ -116685,6 +117919,16 @@ function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: upd
|
|
|
116685
117919
|
// Answers once the install has started, not once it has finished — see the note on
|
|
116686
117920
|
// `updates.apply` in the protocol. The rest arrives as `update_status` events.
|
|
116687
117921
|
"updates.apply": async () => requireUpdates().apply(),
|
|
117922
|
+
// A window's answer and report for an agent's "Apply now" (#665). Without the broker (tests) nothing is waiting
|
|
117923
|
+
"updates.answerApply": async (p, ctx) => {
|
|
117924
|
+
const a = RpcMethods["updates.answerApply"].params.parse(p);
|
|
117925
|
+
return updateApply2 && ctx ? updateApply2.answer(ctx.client, a) : { ticket: null };
|
|
117926
|
+
},
|
|
117927
|
+
"updates.reportApply": async (p, ctx) => {
|
|
117928
|
+
const r = RpcMethods["updates.reportApply"].params.parse(p);
|
|
117929
|
+
if (updateApply2 && ctx) updateApply2.report(ctx.client, r);
|
|
117930
|
+
return {};
|
|
117931
|
+
},
|
|
116688
117932
|
"host.stop": async () => {
|
|
116689
117933
|
if (!stopHost) {
|
|
116690
117934
|
throw Object.assign(new Error("This host runs under the Centralu app, which stops it: quit the app instead"), { code: "internal" });
|
|
@@ -116759,6 +118003,143 @@ function onExternalAppListChanged(apps, emit) {
|
|
|
116759
118003
|
});
|
|
116760
118004
|
}
|
|
116761
118005
|
|
|
118006
|
+
// packages/agent-host/src/update-apply.ts
|
|
118007
|
+
import { randomUUID as randomUUID11 } from "node:crypto";
|
|
118008
|
+
var ANSWER_MS = 5e3;
|
|
118009
|
+
var OUTCOME_MS = 2e4;
|
|
118010
|
+
var GONE_GRACE_MS = 5e3;
|
|
118011
|
+
var NO_WINDOW = "No Centralu window is open, so nothing was relaunched. The installed build starts the next time the person opens Centralu.";
|
|
118012
|
+
var WINDOW_GONE = "The window that was told to relaunch went away before it said it was relaunching, so whether it relaunched is not known. Look at Centralu.";
|
|
118013
|
+
var NO_ANSWER = "No Centralu window answered. A window of an older build cannot apply an update for an agent: ask the person to click Apply now on the update line.";
|
|
118014
|
+
var UpdateApplyBroker = class {
|
|
118015
|
+
constructor(opts) {
|
|
118016
|
+
this.opts = opts;
|
|
118017
|
+
}
|
|
118018
|
+
opts;
|
|
118019
|
+
probes = /* @__PURE__ */ new Map();
|
|
118020
|
+
goes = /* @__PURE__ */ new Map();
|
|
118021
|
+
/**
|
|
118022
|
+
* The whole call: ask, then the caller's consent (null to go on, or why not), then ask again and relaunch one window.
|
|
118023
|
+
* `consent` runs only when a window was ready, so "nothing installed" never raises a card.
|
|
118024
|
+
*/
|
|
118025
|
+
async apply(consent) {
|
|
118026
|
+
const first = await this.probe();
|
|
118027
|
+
if (!first.ready) return { ok: false, error: first.error };
|
|
118028
|
+
const refused = await consent();
|
|
118029
|
+
if (refused) return { ok: false, error: refused };
|
|
118030
|
+
const ready = await this.probe();
|
|
118031
|
+
if (!ready.ready) return { ok: false, error: ready.error };
|
|
118032
|
+
return this.go(ready);
|
|
118033
|
+
}
|
|
118034
|
+
/** Asks every window whether its "Apply now" could relaunch now; resolves with the first ready one */
|
|
118035
|
+
probe() {
|
|
118036
|
+
if (this.opts.windows() === 0) return Promise.resolve({ ready: false, error: NO_WINDOW });
|
|
118037
|
+
const requestId = randomUUID11();
|
|
118038
|
+
return new Promise((resolve12) => {
|
|
118039
|
+
const p = {
|
|
118040
|
+
requestId,
|
|
118041
|
+
answered: /* @__PURE__ */ new Set(),
|
|
118042
|
+
reasons: [],
|
|
118043
|
+
finish: (r) => {
|
|
118044
|
+
if (!this.probes.delete(requestId)) return;
|
|
118045
|
+
clearTimeout(timer);
|
|
118046
|
+
resolve12(r);
|
|
118047
|
+
}
|
|
118048
|
+
};
|
|
118049
|
+
const timer = setTimeout(() => p.finish(this.notReady(p)), this.opts.answerMs ?? ANSWER_MS);
|
|
118050
|
+
this.probes.set(requestId, p);
|
|
118051
|
+
this.opts.push({ type: "update_apply_request", requestId });
|
|
118052
|
+
});
|
|
118053
|
+
}
|
|
118054
|
+
/** `updates.answerApply`: a ticket for the first ready window of a request still waiting, null for every other */
|
|
118055
|
+
answer(client, a) {
|
|
118056
|
+
const p = this.probes.get(a.requestId);
|
|
118057
|
+
if (!p || p.answered.has(client)) return { ticket: null };
|
|
118058
|
+
p.answered.add(client);
|
|
118059
|
+
if (a.ready) {
|
|
118060
|
+
const ticket = randomUUID11();
|
|
118061
|
+
p.finish({ ready: true, requestId: a.requestId, ticket, client, ...a.version ? { version: a.version } : {} });
|
|
118062
|
+
return { ticket };
|
|
118063
|
+
}
|
|
118064
|
+
const reason = a.reason?.trim() || "That window cannot relaunch into the installed build.";
|
|
118065
|
+
if (!p.reasons.includes(reason)) p.reasons.push(reason);
|
|
118066
|
+
if (p.answered.size >= this.opts.windows()) p.finish(this.notReady(p));
|
|
118067
|
+
return { ticket: null };
|
|
118068
|
+
}
|
|
118069
|
+
/**
|
|
118070
|
+
* Tells the picked window to relaunch and waits for its outcome (#665 review). The window says "started" right before
|
|
118071
|
+
* it relaunches; only after that does its connection ending count as the relaunch. Ending before it — a crash, a
|
|
118072
|
+
* reload, a lost socket — is not a success: the window is given `goneGraceMs` to come back and report under the same
|
|
118073
|
+
* ticket, then the answer says whether it relaunched is not known.
|
|
118074
|
+
*/
|
|
118075
|
+
go(ready) {
|
|
118076
|
+
return new Promise((resolve12) => {
|
|
118077
|
+
const g5 = {
|
|
118078
|
+
requestId: ready.requestId,
|
|
118079
|
+
ticket: ready.ticket,
|
|
118080
|
+
client: ready.client,
|
|
118081
|
+
started: false,
|
|
118082
|
+
...ready.version ? { version: ready.version } : {},
|
|
118083
|
+
finish: (o) => {
|
|
118084
|
+
if (!this.goes.delete(ready.ticket)) return;
|
|
118085
|
+
clearTimeout(timer);
|
|
118086
|
+
clearTimeout(g5.grace);
|
|
118087
|
+
resolve12(o);
|
|
118088
|
+
}
|
|
118089
|
+
};
|
|
118090
|
+
const timer = setTimeout(
|
|
118091
|
+
() => g5.finish({
|
|
118092
|
+
ok: false,
|
|
118093
|
+
error: g5.started ? "The window said it was relaunching but is still open. Look at Centralu to see whether it relaunched." : "The window was told to relaunch but did not say whether it did. Look at Centralu to see whether it relaunched."
|
|
118094
|
+
}),
|
|
118095
|
+
this.opts.outcomeMs ?? OUTCOME_MS
|
|
118096
|
+
);
|
|
118097
|
+
this.goes.set(ready.ticket, g5);
|
|
118098
|
+
this.opts.push({ type: "update_apply_go", requestId: ready.requestId, ticket: ready.ticket });
|
|
118099
|
+
});
|
|
118100
|
+
}
|
|
118101
|
+
/**
|
|
118102
|
+
* `updates.reportApply`: the picked window's outcome. Matched by ticket, not by connection: only that window was given
|
|
118103
|
+
* it, and a window that reconnected has a new connection number. A report naming another ticket, or a settled one, is
|
|
118104
|
+
* dropped. The connection it came on is the one whose end is watched from then on.
|
|
118105
|
+
*/
|
|
118106
|
+
report(client, r) {
|
|
118107
|
+
const g5 = this.goes.get(r.ticket);
|
|
118108
|
+
if (!g5 || g5.requestId !== r.requestId) return;
|
|
118109
|
+
g5.client = client;
|
|
118110
|
+
clearTimeout(g5.grace);
|
|
118111
|
+
g5.grace = void 0;
|
|
118112
|
+
if (r.error) return g5.finish({ ok: false, error: r.error });
|
|
118113
|
+
if (r.started) {
|
|
118114
|
+
g5.started = true;
|
|
118115
|
+
return;
|
|
118116
|
+
}
|
|
118117
|
+
g5.finish(this.relaunched(g5));
|
|
118118
|
+
}
|
|
118119
|
+
/** A window's connection ended: after "started" that is the relaunch; before it, unknown; and one less to wait for */
|
|
118120
|
+
clientGone(client) {
|
|
118121
|
+
for (const g5 of [...this.goes.values()]) {
|
|
118122
|
+
if (g5.client !== client) continue;
|
|
118123
|
+
if (g5.started) {
|
|
118124
|
+
g5.finish(this.relaunched(g5));
|
|
118125
|
+
continue;
|
|
118126
|
+
}
|
|
118127
|
+
clearTimeout(g5.grace);
|
|
118128
|
+
g5.grace = setTimeout(() => g5.finish({ ok: false, error: WINDOW_GONE }), this.opts.goneGraceMs ?? GONE_GRACE_MS);
|
|
118129
|
+
}
|
|
118130
|
+
for (const p of [...this.probes.values()]) {
|
|
118131
|
+
if (p.answered.size >= this.opts.windows()) p.finish(this.notReady(p));
|
|
118132
|
+
}
|
|
118133
|
+
}
|
|
118134
|
+
relaunched(g5) {
|
|
118135
|
+
return { ok: true, ...g5.version ? { version: g5.version } : {} };
|
|
118136
|
+
}
|
|
118137
|
+
notReady(p) {
|
|
118138
|
+
if (p.reasons.length === 0) return { ready: false, error: this.opts.windows() === 0 ? NO_WINDOW : NO_ANSWER };
|
|
118139
|
+
return { ready: false, error: `Nothing was relaunched. ${p.reasons.join(" ")}` };
|
|
118140
|
+
}
|
|
118141
|
+
};
|
|
118142
|
+
|
|
116762
118143
|
// packages/agent-host/src/app-change-events.ts
|
|
116763
118144
|
var APP_CHANGE_WINDOW_MS = 250;
|
|
116764
118145
|
function broadcastAppChanges(send, windowMs = APP_CHANGE_WINDOW_MS) {
|
|
@@ -117228,14 +118609,16 @@ var CommandRunner = class {
|
|
|
117228
118609
|
};
|
|
117229
118610
|
|
|
117230
118611
|
// packages/agent-host/src/updates.ts
|
|
117231
|
-
import { execFile as execFile8 } from "node:child_process";
|
|
117232
|
-
import { existsSync as existsSync21 } from "node:fs";
|
|
118612
|
+
import { execFile as execFile8, execFileSync as execFileSync4 } from "node:child_process";
|
|
118613
|
+
import { existsSync as existsSync21, readFileSync as readFileSync13 } from "node:fs";
|
|
118614
|
+
import { join as join31 } from "node:path";
|
|
117233
118615
|
var REGISTRY_URL = `https://registry.npmjs.org/${APP_SLUG}/latest`;
|
|
117234
118616
|
var CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
117235
118617
|
var SCHEDULED_TIMEOUT_MS = 3e3;
|
|
117236
118618
|
var FORCED_TIMEOUT_MS = 8e3;
|
|
117237
118619
|
var INSTALL_CHECK_MAX_AGE_MS = 6e4;
|
|
117238
118620
|
var STORED_RELEASE_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
118621
|
+
var AUTO_RETRY_DELAYS_MS = [10 * 60 * 1e3, 60 * 60 * 1e3, 6 * 60 * 60 * 1e3];
|
|
117239
118622
|
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
117240
118623
|
var NPM_RETRY_DELAY_MS = 3e4;
|
|
117241
118624
|
var NPM_RETRY_WINDOW_MS = 10 * 60 * 1e3;
|
|
@@ -117243,6 +118626,16 @@ function existingInstalledCopy() {
|
|
|
117243
118626
|
const path2 = installedCopyPath();
|
|
117244
118627
|
return existsSync21(path2) ? path2 : null;
|
|
117245
118628
|
}
|
|
118629
|
+
function readInstalledCopyVersion(copy, platform = HOST_PLATFORM) {
|
|
118630
|
+
const source = installedCopyVersionSource(copy, platform);
|
|
118631
|
+
if (!source) return null;
|
|
118632
|
+
try {
|
|
118633
|
+
const text2 = source.kind === "command" ? execFileSync4(source.file, source.args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3, windowsHide: true }) : readFileSync13(join31(copy, source.name), "utf8");
|
|
118634
|
+
return text2.trim() || null;
|
|
118635
|
+
} catch {
|
|
118636
|
+
return null;
|
|
118637
|
+
}
|
|
118638
|
+
}
|
|
117246
118639
|
async function fetchLatestFromRegistry(timeoutMs) {
|
|
117247
118640
|
try {
|
|
117248
118641
|
const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(timeoutMs) });
|
|
@@ -117282,7 +118675,13 @@ var UpdateService = class {
|
|
|
117282
118675
|
fetchLatest: deps.fetchLatest ?? fetchLatestFromRegistry,
|
|
117283
118676
|
run: deps.run ?? runCommand,
|
|
117284
118677
|
installedCopy: deps.installedCopy ?? existingInstalledCopy,
|
|
118678
|
+
installedCopyVersion: deps.installedCopyVersion ?? ((copy) => readInstalledCopyVersion(copy)),
|
|
117285
118679
|
now: deps.now ?? Date.now,
|
|
118680
|
+
schedule: deps.schedule ?? ((fn2, ms3) => {
|
|
118681
|
+
const t = setTimeout(fn2, ms3);
|
|
118682
|
+
t.unref?.();
|
|
118683
|
+
return () => clearTimeout(t);
|
|
118684
|
+
}),
|
|
117286
118685
|
// If no place to persist it is given, treat auto-update as on — the same default that main.ts
|
|
117287
118686
|
// passes in.
|
|
117288
118687
|
readAuto: deps.readAuto ?? (() => true),
|
|
@@ -117322,6 +118721,10 @@ var UpdateService = class {
|
|
|
117322
118721
|
deps;
|
|
117323
118722
|
/** One check at a time — the timer and the button can land together */
|
|
117324
118723
|
inFlight = null;
|
|
118724
|
+
/** The last failed install: which version, how many times in a row, and when (#645) */
|
|
118725
|
+
retry = null;
|
|
118726
|
+
/** Cancels the automatic retry timer, while one is pending */
|
|
118727
|
+
retryTimer = null;
|
|
117325
118728
|
current() {
|
|
117326
118729
|
return { ...this.status };
|
|
117327
118730
|
}
|
|
@@ -117342,6 +118745,7 @@ var UpdateService = class {
|
|
|
117342
118745
|
stop() {
|
|
117343
118746
|
this.stopped = true;
|
|
117344
118747
|
this.stopTimer();
|
|
118748
|
+
this.cancelRetry();
|
|
117345
118749
|
}
|
|
117346
118750
|
stopTimer() {
|
|
117347
118751
|
if (this.timer) clearInterval(this.timer);
|
|
@@ -117385,13 +118789,46 @@ var UpdateService = class {
|
|
|
117385
118789
|
this.deps.writeAutoApply(enabled);
|
|
117386
118790
|
this.emit();
|
|
117387
118791
|
if (enabled) this.installIfAutomatic();
|
|
118792
|
+
else this.cancelRetry();
|
|
117388
118793
|
return this.current();
|
|
117389
118794
|
}
|
|
117390
|
-
/**
|
|
118795
|
+
/**
|
|
118796
|
+
* With `autoApply` on and a newer version known, install it — once, not over an install or a
|
|
118797
|
+
* finished one. A failed install is tried again, but only on the backoff (#645): a check that
|
|
118798
|
+
* lands right after the failure does not start another `npm i -g`, and one after the delay does,
|
|
118799
|
+
* whether the retry timer or a person's Check now brought it.
|
|
118800
|
+
*/
|
|
117391
118801
|
installIfAutomatic() {
|
|
117392
118802
|
if (!this.status.autoApply || !this.status.newer || !this.status.latest) return;
|
|
117393
|
-
if (this.status.phase
|
|
117394
|
-
|
|
118803
|
+
if (this.status.phase === "idle") {
|
|
118804
|
+
void this.apply();
|
|
118805
|
+
return;
|
|
118806
|
+
}
|
|
118807
|
+
if (this.status.phase !== "failed") return;
|
|
118808
|
+
if (this.retryDue()) void this.apply();
|
|
118809
|
+
else if (!this.retryTimer) this.scheduleRetry();
|
|
118810
|
+
}
|
|
118811
|
+
/**
|
|
118812
|
+
* A failed install may be retried once its backoff has passed — or at once for a release that
|
|
118813
|
+
* is not the one that failed: a new version is a new situation, and one attempt at it is cheap.
|
|
118814
|
+
*/
|
|
118815
|
+
retryDue() {
|
|
118816
|
+
if (!this.retry || this.retry.version !== this.status.latest) return true;
|
|
118817
|
+
return this.deps.now() - this.retry.at >= retryDelay(this.retry.failures);
|
|
118818
|
+
}
|
|
118819
|
+
scheduleRetry() {
|
|
118820
|
+
this.cancelRetry();
|
|
118821
|
+
if (this.stopped || !this.retry || !this.status.autoApply || this.status.phase !== "failed") return;
|
|
118822
|
+
const delay = Math.max(0, retryDelay(this.retry.failures) - (this.deps.now() - this.retry.at));
|
|
118823
|
+
this.retryTimer = this.deps.schedule(() => {
|
|
118824
|
+
this.retryTimer = null;
|
|
118825
|
+
if (this.stopped || !this.status.autoApply || this.status.phase !== "failed" || !this.status.newer || !this.status.latest) return;
|
|
118826
|
+
void this.apply();
|
|
118827
|
+
}, delay);
|
|
118828
|
+
}
|
|
118829
|
+
cancelRetry() {
|
|
118830
|
+
this.retryTimer?.();
|
|
118831
|
+
this.retryTimer = null;
|
|
117395
118832
|
}
|
|
117396
118833
|
/**
|
|
117397
118834
|
* Look at the registry, or answer with what we already know.
|
|
@@ -117441,63 +118878,94 @@ var UpdateService = class {
|
|
|
117441
118878
|
* Install the newer version, then say so. **Does not restart the app** — that is the
|
|
117442
118879
|
* window's step (#352).
|
|
117443
118880
|
*
|
|
117444
|
-
*
|
|
117445
|
-
*
|
|
117446
|
-
*
|
|
118881
|
+
* Answers once the target is settled: after the fresh lookup (at most the forced deadline, plus
|
|
118882
|
+
* an older check it waits for), when `npm i -g` is about to start, or when the registry shows
|
|
118883
|
+
* nothing newer. Not when the install is done — `npm i -g` regularly takes longer than the RPC
|
|
118884
|
+
* deadline, and a caller that has already given up cannot be told how it went; the rest of the
|
|
118885
|
+
* story arrives as `update_status` events.
|
|
118886
|
+
*
|
|
118887
|
+
* With nothing newer known it refreshes first rather than refusing (#645): the stored answer
|
|
118888
|
+
* may be hours old, or there may be none at all with automatic checks off. "Up to date" is then
|
|
118889
|
+
* a result (`idle`, no error), never `failed` — a refusal recorded as a failure stayed on the
|
|
118890
|
+
* line through every later check.
|
|
117447
118891
|
*/
|
|
117448
118892
|
apply() {
|
|
117449
|
-
if (this.stopped || this.installing || this.status.phase === "restart_required") return this.current();
|
|
117450
|
-
const latest = this.status.latest;
|
|
117451
|
-
if (!this.status.newer || !latest) {
|
|
117452
|
-
this.status = { ...this.status, phase: "failed", error: "There is no newer version to install" };
|
|
117453
|
-
this.emit();
|
|
117454
|
-
return this.current();
|
|
117455
|
-
}
|
|
118893
|
+
if (this.stopped || this.installing || this.status.phase === "restart_required") return Promise.resolve(this.current());
|
|
117456
118894
|
this.installing = true;
|
|
117457
|
-
this.
|
|
118895
|
+
this.cancelRetry();
|
|
118896
|
+
const known = this.status.newer && this.status.latest ? this.status.latest : null;
|
|
118897
|
+
this.status = { ...this.status, phase: known ? "updating" : "checking", error: null };
|
|
117458
118898
|
this.emit();
|
|
117459
|
-
void this.runApply(
|
|
117460
|
-
return this.current();
|
|
118899
|
+
return new Promise((settled) => void this.runApply(known, settled));
|
|
117461
118900
|
}
|
|
117462
|
-
async runApply(
|
|
118901
|
+
async runApply(known, settled) {
|
|
118902
|
+
let version2 = known;
|
|
117463
118903
|
let packageInstalled = false;
|
|
118904
|
+
let answered = false;
|
|
118905
|
+
const answer = () => {
|
|
118906
|
+
if (answered) return;
|
|
118907
|
+
answered = true;
|
|
118908
|
+
settled(this.current());
|
|
118909
|
+
};
|
|
117464
118910
|
const copy = this.deps.installedCopy();
|
|
117465
118911
|
try {
|
|
117466
118912
|
await this.inFlight;
|
|
117467
118913
|
if (this.stopped) return;
|
|
117468
118914
|
const age2 = this.status.checkedAt === null ? Infinity : this.deps.now() - this.status.checkedAt;
|
|
117469
|
-
if (age2 < 0 || age2 >= INSTALL_CHECK_MAX_AGE_MS) {
|
|
118915
|
+
if (version2 === null || age2 < 0 || age2 >= INSTALL_CHECK_MAX_AGE_MS) {
|
|
117470
118916
|
const fresh = await this.deps.fetchLatest(FORCED_TIMEOUT_MS);
|
|
117471
118917
|
if (this.stopped) return;
|
|
117472
118918
|
if (fresh.ok) {
|
|
117473
118919
|
const now = this.deps.now();
|
|
117474
118920
|
const storedAge = this.status.checkedAt === null ? Infinity : now - this.status.checkedAt;
|
|
117475
|
-
const keepStored = storedAge >= 0 && storedAge < STORED_RELEASE_MAX_AGE_MS && isNewerVersion(version2, fresh.version);
|
|
117476
|
-
|
|
117477
|
-
|
|
118921
|
+
const keepStored = version2 !== null && storedAge >= 0 && storedAge < STORED_RELEASE_MAX_AGE_MS && isNewerVersion(version2, fresh.version);
|
|
118922
|
+
const target = keepStored && version2 !== null ? version2 : fresh.version;
|
|
118923
|
+
version2 = target;
|
|
118924
|
+
this.status = { ...this.status, latest: target, newer: isNewerVersion(target, this.status.current), checkedAt: keepStored ? this.status.checkedAt : now };
|
|
117478
118925
|
if (!this.status.newer) {
|
|
117479
118926
|
this.status = { ...this.status, phase: "idle", error: null };
|
|
117480
118927
|
return;
|
|
117481
118928
|
}
|
|
118929
|
+
} else if (version2 === null) {
|
|
118930
|
+
this.status = { ...this.status, phase: "idle", error: fresh.reason };
|
|
118931
|
+
return;
|
|
117482
118932
|
} else {
|
|
117483
118933
|
this.status = { ...this.status, error: `Could not refresh latest (${fresh.reason}); using previously checked ${version2}.` };
|
|
117484
118934
|
}
|
|
117485
118935
|
}
|
|
118936
|
+
if (version2 === null) return;
|
|
118937
|
+
this.status = { ...this.status, phase: "updating" };
|
|
117486
118938
|
this.emit();
|
|
118939
|
+
answer();
|
|
117487
118940
|
await this.installPackage(version2);
|
|
117488
118941
|
if (this.stopped) return;
|
|
117489
118942
|
packageInstalled = true;
|
|
117490
|
-
if (copy)
|
|
118943
|
+
if (copy) {
|
|
118944
|
+
const copyVersion = this.deps.installedCopyVersion(copy);
|
|
118945
|
+
if (keepsInstalledCopy(version2, copyVersion)) {
|
|
118946
|
+
const notice = `The local build ${copyVersion} at ${copy} is newer than ${version2} and was kept.`;
|
|
118947
|
+
this.status = { ...this.status, error: this.status.error ? `${this.status.error} ${notice}` : notice };
|
|
118948
|
+
} else {
|
|
118949
|
+
await this.deps.run(APP_SLUG, ["install"]);
|
|
118950
|
+
}
|
|
118951
|
+
}
|
|
117491
118952
|
if (this.stopped) return;
|
|
117492
118953
|
this.status = { ...this.status, phase: "restart_required" };
|
|
118954
|
+
this.retry = null;
|
|
117493
118955
|
} catch (e) {
|
|
117494
118956
|
if (this.stopped) return;
|
|
117495
118957
|
const reason = e.message;
|
|
117496
118958
|
const error51 = packageInstalled ? `npm installed ${version2}, but the copy at ${copy} was not replaced (${reason}). Quit ${APP_NAME}, then run "${APP_SLUG} install" in a terminal.` : reason;
|
|
117497
118959
|
this.status = { ...this.status, phase: "failed", error: error51 };
|
|
118960
|
+
const target = version2 ?? this.status.latest ?? "";
|
|
118961
|
+
this.retry = { version: target, failures: this.retry?.version === target ? this.retry.failures + 1 : 1, at: this.deps.now() };
|
|
117498
118962
|
} finally {
|
|
117499
118963
|
this.installing = false;
|
|
117500
|
-
if (!this.stopped)
|
|
118964
|
+
if (!this.stopped) {
|
|
118965
|
+
this.emit();
|
|
118966
|
+
if (this.status.phase === "failed") this.installIfAutomatic();
|
|
118967
|
+
}
|
|
118968
|
+
answer();
|
|
117501
118969
|
}
|
|
117502
118970
|
}
|
|
117503
118971
|
/**
|
|
@@ -117537,6 +119005,9 @@ var UpdateService = class {
|
|
|
117537
119005
|
function npmHasNotServedVersion(err) {
|
|
117538
119006
|
return /(?:\bETARGET\b|\bnotarget\b)/i.test(err instanceof Error ? err.message : String(err));
|
|
117539
119007
|
}
|
|
119008
|
+
function retryDelay(failures) {
|
|
119009
|
+
return AUTO_RETRY_DELAYS_MS[Math.min(Math.max(failures, 1), AUTO_RETRY_DELAYS_MS.length) - 1];
|
|
119010
|
+
}
|
|
117540
119011
|
|
|
117541
119012
|
// packages/agent-host/src/agent-versions.ts
|
|
117542
119013
|
var CHECK_INTERVAL_MS2 = 10 * 60 * 1e3;
|
|
@@ -117879,16 +119350,16 @@ var SessionSleeper = class {
|
|
|
117879
119350
|
};
|
|
117880
119351
|
|
|
117881
119352
|
// packages/agent-host/src/dev-services/instance-lock.ts
|
|
117882
|
-
import { execFileSync as
|
|
117883
|
-
import { readFileSync as
|
|
117884
|
-
import { dirname as
|
|
119353
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
119354
|
+
import { readFileSync as readFileSync14, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7 } from "node:fs";
|
|
119355
|
+
import { dirname as dirname11, join as join32 } from "node:path";
|
|
117885
119356
|
import Database2 from "better-sqlite3";
|
|
117886
119357
|
var OWNERSHIP_FILE = "host-ownership.sqlite";
|
|
117887
119358
|
var held = /* @__PURE__ */ new Set();
|
|
117888
119359
|
function processStartTime(pid) {
|
|
117889
119360
|
if (!hasPs()) return null;
|
|
117890
119361
|
try {
|
|
117891
|
-
const out =
|
|
119362
|
+
const out = execFileSync5("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
117892
119363
|
encoding: "utf8",
|
|
117893
119364
|
timeout: 2e3,
|
|
117894
119365
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -117915,15 +119386,15 @@ function parseHolder(raw) {
|
|
|
117915
119386
|
function acquireInstanceLock(dbPath2, startOf = processStartTime) {
|
|
117916
119387
|
if (dbPath2 === ":memory:") return { ok: true, release: () => {
|
|
117917
119388
|
} };
|
|
117918
|
-
const lockPath =
|
|
119389
|
+
const lockPath = join32(dirname11(dbPath2), "host.lock");
|
|
117919
119390
|
const readHolder = () => {
|
|
117920
119391
|
try {
|
|
117921
|
-
return parseHolder(
|
|
119392
|
+
return parseHolder(readFileSync14(lockPath, "utf8"));
|
|
117922
119393
|
} catch {
|
|
117923
119394
|
return null;
|
|
117924
119395
|
}
|
|
117925
119396
|
};
|
|
117926
|
-
const owner = takeOwnership(
|
|
119397
|
+
const owner = takeOwnership(join32(dirname11(dbPath2), OWNERSHIP_FILE));
|
|
117927
119398
|
if (!owner) {
|
|
117928
119399
|
const holder = readHolder();
|
|
117929
119400
|
return { ok: false, heldByPid: holder && Number.isInteger(holder.pid) && holder.pid > 0 ? holder.pid : null, lockPath };
|
|
@@ -117936,7 +119407,7 @@ function acquireInstanceLock(dbPath2, startOf = processStartTime) {
|
|
|
117936
119407
|
if (released) return;
|
|
117937
119408
|
released = true;
|
|
117938
119409
|
try {
|
|
117939
|
-
if (parseHolder(
|
|
119410
|
+
if (parseHolder(readFileSync14(lockPath, "utf8")).pid === process.pid) unlinkSync3(lockPath);
|
|
117940
119411
|
} catch {
|
|
117941
119412
|
}
|
|
117942
119413
|
giveUp(owner);
|
|
@@ -118351,7 +119822,7 @@ async function runDrain(bound, deps) {
|
|
|
118351
119822
|
}
|
|
118352
119823
|
|
|
118353
119824
|
// packages/agent-host/src/links/install.ts
|
|
118354
|
-
import { existsSync as existsSync22, readFileSync as
|
|
119825
|
+
import { existsSync as existsSync22, readFileSync as readFileSync15 } from "node:fs";
|
|
118355
119826
|
import { hostname as hostname3 } from "node:os";
|
|
118356
119827
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
118357
119828
|
import { gzipSync } from "node:zlib";
|
|
@@ -119029,7 +120500,7 @@ async function installRemote(o) {
|
|
|
119029
120500
|
function bundled(name, source) {
|
|
119030
120501
|
for (const url2 of [new URL(`./${name}`, import.meta.url), new URL(source, import.meta.url)]) {
|
|
119031
120502
|
const file2 = fileURLToPath6(url2);
|
|
119032
|
-
if (existsSync22(file2)) return
|
|
120503
|
+
if (existsSync22(file2)) return readFileSync15(file2, "utf8");
|
|
119033
120504
|
}
|
|
119034
120505
|
throw new Error(`${name} is missing from this host`);
|
|
119035
120506
|
}
|
|
@@ -119156,9 +120627,9 @@ function probeTarget(target, opts = {}) {
|
|
|
119156
120627
|
|
|
119157
120628
|
// packages/agent-host/src/links/ssh-config.ts
|
|
119158
120629
|
import { spawn as spawn8 } from "node:child_process";
|
|
119159
|
-
import { readdirSync as readdirSync8, readFileSync as
|
|
120630
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync16, statSync as statSync13 } from "node:fs";
|
|
119160
120631
|
import { homedir as homedir13 } from "node:os";
|
|
119161
|
-
import { isAbsolute as isAbsolute10, join as
|
|
120632
|
+
import { isAbsolute as isAbsolute10, join as join33, normalize, parse as parse6, sep as sep12 } from "node:path";
|
|
119162
120633
|
var MAX_DEPTH2 = 16;
|
|
119163
120634
|
function splitConfigLine(raw) {
|
|
119164
120635
|
const m = /^\s*([^\s=#]+)\s*(?:=\s*)?([\s\S]*)$/.exec(raw);
|
|
@@ -119224,7 +120695,7 @@ function expandGlob(path2) {
|
|
|
119224
120695
|
const next = [];
|
|
119225
120696
|
for (const base2 of found) {
|
|
119226
120697
|
if (!MAGIC.test(seg)) {
|
|
119227
|
-
next.push(
|
|
120698
|
+
next.push(join33(base2, seg));
|
|
119228
120699
|
continue;
|
|
119229
120700
|
}
|
|
119230
120701
|
let names;
|
|
@@ -119236,7 +120707,7 @@ function expandGlob(path2) {
|
|
|
119236
120707
|
const re2 = segmentRe(seg);
|
|
119237
120708
|
for (const name of names.sort()) {
|
|
119238
120709
|
if (name.startsWith(".") && !seg.startsWith(".")) continue;
|
|
119239
|
-
if (re2.test(name)) next.push(
|
|
120710
|
+
if (re2.test(name)) next.push(join33(base2, name));
|
|
119240
120711
|
}
|
|
119241
120712
|
}
|
|
119242
120713
|
found = next;
|
|
@@ -119266,7 +120737,7 @@ function readSshConfig(opts) {
|
|
|
119266
120737
|
}
|
|
119267
120738
|
let text2;
|
|
119268
120739
|
try {
|
|
119269
|
-
text2 =
|
|
120740
|
+
text2 = readFileSync16(file2, "utf8");
|
|
119270
120741
|
} catch (err) {
|
|
119271
120742
|
if (depth > 0 && err.code !== "ENOENT") notes.push(`${tildePath(file2, opts.home)} could not be read`);
|
|
119272
120743
|
return;
|
|
@@ -119291,11 +120762,11 @@ function readSshConfig(opts) {
|
|
|
119291
120762
|
}
|
|
119292
120763
|
for (const arg of split2.args) {
|
|
119293
120764
|
let path2 = arg;
|
|
119294
|
-
if (path2.startsWith("~/")) path2 =
|
|
120765
|
+
if (path2.startsWith("~/")) path2 = join33(opts.home, path2.slice(2));
|
|
119295
120766
|
else if (path2.startsWith("~")) {
|
|
119296
120767
|
notes.push(`An Include of another user's folder (${where(file2, n + 1)}) was not read`);
|
|
119297
120768
|
continue;
|
|
119298
|
-
} else if (!isAbsolute10(path2)) path2 =
|
|
120769
|
+
} else if (!isAbsolute10(path2)) path2 = join33(opts.home, ".ssh", path2);
|
|
119299
120770
|
for (const f of expandGlob(path2)) read(f, depth + 1);
|
|
119300
120771
|
}
|
|
119301
120772
|
}
|
|
@@ -119345,7 +120816,7 @@ var MAX_HOSTS = 100;
|
|
|
119345
120816
|
var RESOLVE_AT_ONCE = 8;
|
|
119346
120817
|
async function listSshHosts(opts = {}) {
|
|
119347
120818
|
const home = opts.home ?? homedir13();
|
|
119348
|
-
const file2 = opts.configFile ??
|
|
120819
|
+
const file2 = opts.configFile ?? join33(home, ".ssh", "config");
|
|
119349
120820
|
const read = readSshConfig({ file: file2, home });
|
|
119350
120821
|
const notes = [...read.notes];
|
|
119351
120822
|
const listed = read.hosts.slice(0, MAX_HOSTS);
|
|
@@ -119708,7 +121179,10 @@ var Qualifier = class {
|
|
|
119708
121179
|
const sid = typeof e.sessionId === "string" ? this.id(e.sessionId) : void 0;
|
|
119709
121180
|
const base2 = { ...e, ...sid !== void 0 ? { sessionId: sid } : {} };
|
|
119710
121181
|
switch (e.type) {
|
|
121182
|
+
// Another machine's agent asking its own windows to apply (#665) must never relaunch the hub's window
|
|
119711
121183
|
case "update_status":
|
|
121184
|
+
case "update_apply_request":
|
|
121185
|
+
case "update_apply_go":
|
|
119712
121186
|
case "themes_changed":
|
|
119713
121187
|
case "app_state_changed":
|
|
119714
121188
|
case "agent_versions":
|
|
@@ -120733,7 +122207,7 @@ function linkedSessions(machines, route = notRouted, hubName2) {
|
|
|
120733
122207
|
async updateSettings(sessionId, s) {
|
|
120734
122208
|
const m = owner(sessionId);
|
|
120735
122209
|
if (!m) return { ok: false, error: `Not a session of a linked machine: ${sessionId}` };
|
|
120736
|
-
return attempt(m, async () => ({ info: await route("agents.updateSettings", { sessionId, ...s }) }));
|
|
122210
|
+
return attempt(m, async () => ({ info: await route("agents.updateSettings", { sessionId, ...s, by: "orchestrator" }) }));
|
|
120737
122211
|
},
|
|
120738
122212
|
async findProjects(ref) {
|
|
120739
122213
|
const wanted = ref.trim();
|
|
@@ -121128,6 +122602,9 @@ var ROUTES = {
|
|
|
121128
122602
|
"updates.setAuto": HUB,
|
|
121129
122603
|
"updates.setAutoApply": HUB,
|
|
121130
122604
|
"updates.apply": HUB,
|
|
122605
|
+
// The hub's own windows answer the hub's own host (#665); a linked machine's events never ask them
|
|
122606
|
+
"updates.answerApply": HUB,
|
|
122607
|
+
"updates.reportApply": HUB,
|
|
121131
122608
|
"machines.list": HUB,
|
|
121132
122609
|
"machines.add": HUB,
|
|
121133
122610
|
"machines.sshHosts": HUB,
|
|
@@ -121451,7 +122928,7 @@ function storeMirror(store2) {
|
|
|
121451
122928
|
var start = new StartSequence();
|
|
121452
122929
|
start.at("launch variables");
|
|
121453
122930
|
var launch = takeLaunchEnv();
|
|
121454
|
-
var BUILD = true ? "
|
|
122931
|
+
var BUILD = true ? "29ffab5" : "dev";
|
|
121455
122932
|
var { values } = parseArgs({
|
|
121456
122933
|
options: {
|
|
121457
122934
|
port: { type: "string", default: "5175" },
|
|
@@ -121472,17 +122949,17 @@ start.at("data folder");
|
|
|
121472
122949
|
var movedNote = null;
|
|
121473
122950
|
function defaultDbPath() {
|
|
121474
122951
|
const isDev = process.env.CC_DEV === "1";
|
|
121475
|
-
const dir =
|
|
121476
|
-
const legacy =
|
|
122952
|
+
const dir = join34(homedir14(), isDev ? DATA_DIR_DEV : DATA_DIR);
|
|
122953
|
+
const legacy = join34(homedir14(), isDev ? DATA_DIR_LEGACY.dev : DATA_DIR_LEGACY.prod);
|
|
121477
122954
|
movedNote = migrateLegacyDataDir(legacy, dir) ? `[agent-host] data folder moved: ${legacy} -> ${dir}` : null;
|
|
121478
122955
|
mkdirSync11(dir, { recursive: true });
|
|
121479
122956
|
process.env.CC_DATA_DIR = dir;
|
|
121480
|
-
return
|
|
122957
|
+
return join34(dir, "store.db");
|
|
121481
122958
|
}
|
|
121482
122959
|
var dbPath = values.memory ? ":memory:" : values.db ?? defaultDbPath();
|
|
121483
122960
|
start.at("log");
|
|
121484
122961
|
var stopLog = dbPath === ":memory:" ? () => {
|
|
121485
|
-
} : teeStderrToFile(hostLogPath(
|
|
122962
|
+
} : teeStderrToFile(hostLogPath(dirname12(dbPath)));
|
|
121486
122963
|
if (dbPath !== ":memory:") console.error(startupBanner({ build: BUILD, db: dbPath, pid: process.pid }));
|
|
121487
122964
|
if (movedNote) console.error(movedNote);
|
|
121488
122965
|
start.at("path");
|
|
@@ -121538,7 +123015,7 @@ try {
|
|
|
121538
123015
|
}
|
|
121539
123016
|
var adapters = createAdapters();
|
|
121540
123017
|
start.at("keeper children");
|
|
121541
|
-
var held2 = launch.underKeeper ? await connectHeldChildren(
|
|
123018
|
+
var held2 = launch.underKeeper ? await connectHeldChildren(dirname12(dbPath)) : null;
|
|
121542
123019
|
start.at("services");
|
|
121543
123020
|
var screens = new Gate();
|
|
121544
123021
|
var events = new Gate();
|
|
@@ -121555,7 +123032,7 @@ var mgr = new SessionManager(
|
|
|
121555
123032
|
},
|
|
121556
123033
|
(sessionId) => bridgeAddress(launch.frontDoor, port, token, sessionId),
|
|
121557
123034
|
// Worktrees are created next to the data folder — dev and the packaged app never touch each other's worktrees
|
|
121558
|
-
|
|
123035
|
+
join34(dirname12(dbPath), "worktrees"),
|
|
121559
123036
|
held2 ? { processes: held2.processes, keptSessions: held2.kept.sessionIds } : {}
|
|
121560
123037
|
);
|
|
121561
123038
|
mgr.useProjectsHome(() => projectsHome());
|
|
@@ -121589,7 +123066,7 @@ if (held2) {
|
|
|
121589
123066
|
}
|
|
121590
123067
|
var AUTO_UPDATE_CHECK_KEY = "updates.auto";
|
|
121591
123068
|
var AUTO_APPLY_UPDATES_KEY = "updates.autoApply";
|
|
121592
|
-
var themes = new ThemeFiles(
|
|
123069
|
+
var themes = new ThemeFiles(join34(dataRoot(), "themes"), () => events.push({ type: "themes_changed" }));
|
|
121593
123070
|
await themes.start().catch((e) => console.error(`[themes] the themes folder is unavailable: ${e.message}`));
|
|
121594
123071
|
var updates = new UpdateService((status) => events.push({ type: "update_status", status }), {
|
|
121595
123072
|
// On by default when nothing has been saved yet. Since the check is read-only and every failure
|
|
@@ -121603,6 +123080,8 @@ var updates = new UpdateService((status) => events.push({ type: "update_status",
|
|
|
121603
123080
|
writeAutoApply: (enabled) => store.setAppSetting(AUTO_APPLY_UPDATES_KEY, String(enabled))
|
|
121604
123081
|
});
|
|
121605
123082
|
mgr.useUpdates(updates);
|
|
123083
|
+
var updateApply = new UpdateApplyBroker({ push: (e) => events.push(e), windows: () => server.clientCount });
|
|
123084
|
+
mgr.useUpdateApply(updateApply);
|
|
121606
123085
|
var AUTO_APPLY_AGENT_VERSIONS_KEY = "agents.autoApplyVersions";
|
|
121607
123086
|
var AGENT_VERSIONS_SEEN_KEY = "agents.versionsSeen";
|
|
121608
123087
|
var agentVersions = new AgentVersionService({
|
|
@@ -121710,7 +123189,8 @@ var router = new Router({
|
|
|
121710
123189
|
heldPids: held2?.heldPids,
|
|
121711
123190
|
machines: links,
|
|
121712
123191
|
stopHost: launch.startedByServe && !launch.underKeeper ? () => stopFromRequest() : void 0,
|
|
121713
|
-
activity: () => activityCounts(activity())
|
|
123192
|
+
activity: () => activityCounts(activity()),
|
|
123193
|
+
updateApply
|
|
121714
123194
|
}),
|
|
121715
123195
|
machines: () => links.all()
|
|
121716
123196
|
});
|
|
@@ -121721,7 +123201,10 @@ var server = new HostServer({
|
|
|
121721
123201
|
allowedOrigins,
|
|
121722
123202
|
onRpc: router.handle,
|
|
121723
123203
|
// A window's connection ended: the pinned views it leased wait for a claim, then close (#392, view-host.ts)
|
|
121724
|
-
onClientGone: (client) =>
|
|
123204
|
+
onClientGone: (client) => {
|
|
123205
|
+
views.holderGone(client);
|
|
123206
|
+
updateApply.clientGone(client);
|
|
123207
|
+
},
|
|
121725
123208
|
// Every HTTP route sits behind this secret (transport/http.ts)
|
|
121726
123209
|
http: { secret: httpSecret, routes: views.routes },
|
|
121727
123210
|
// Which build this is and where it came from, in every hello_ok (#280, keeper-link.ts)
|
|
@@ -121808,7 +123291,7 @@ var endings = wireEndings({
|
|
|
121808
123291
|
heldChildren: held2 !== null,
|
|
121809
123292
|
leave: (mode, handOver) => void shutdown.leave(mode, handOver),
|
|
121810
123293
|
force: (why) => shutdown.force(why),
|
|
121811
|
-
record: crashRecorder(
|
|
123294
|
+
record: crashRecorder(join34(dirname12(dbPath), "host-errors.log"), (line) => console.error(line)),
|
|
121812
123295
|
stdin: process.stdin,
|
|
121813
123296
|
watchParent: values["watch-parent"] === true,
|
|
121814
123297
|
log: (line) => console.error(line)
|