@centralu/darwin-arm64 0.1.0-beta.21 → 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/Centralu.app/Contents/Info.plist +2 -2
- package/Centralu.app/Contents/MacOS/centralu +0 -0
- package/Centralu.app/Contents/MacOS/centralu-keeper +0 -0
- package/Centralu.app/Contents/Resources/content/centralu-keeper +0 -0
- package/Centralu.app/Contents/Resources/content/content-manifest.json +8 -8
- package/Centralu.app/Contents/Resources/content/content-manifest.json.sig +1 -1
- package/Centralu.app/Contents/Resources/content/host/bundle-info.json +2 -2
- package/Centralu.app/Contents/Resources/content/host/codex-orchestrator-bridge.mjs +3 -1
- package/Centralu.app/Contents/Resources/content/host/main.mjs +2098 -362
- package/Centralu.app/Contents/Resources/resources/host/bundle-info.json +2 -2
- package/Centralu.app/Contents/Resources/resources/host/codex-orchestrator-bridge.mjs +3 -1
- package/Centralu.app/Contents/Resources/resources/host/main.mjs +2098 -362
- package/Centralu.app/Contents/_CodeSignature/CodeResources +20 -20
- package/package.json +1 -1
|
@@ -3235,7 +3235,7 @@ var require_stream = __commonJS({
|
|
|
3235
3235
|
};
|
|
3236
3236
|
duplex._final = function(callback) {
|
|
3237
3237
|
if (ws2.readyState === ws2.CONNECTING) {
|
|
3238
|
-
ws2.once("open", function
|
|
3238
|
+
ws2.once("open", function open6() {
|
|
3239
3239
|
duplex._final(callback);
|
|
3240
3240
|
});
|
|
3241
3241
|
return;
|
|
@@ -3256,7 +3256,7 @@ var require_stream = __commonJS({
|
|
|
3256
3256
|
};
|
|
3257
3257
|
duplex._write = function(chunk, encoding, callback) {
|
|
3258
3258
|
if (ws2.readyState === ws2.CONNECTING) {
|
|
3259
|
-
ws2.once("open", function
|
|
3259
|
+
ws2.once("open", function open6() {
|
|
3260
3260
|
duplex._write(chunk, encoding, callback);
|
|
3261
3261
|
});
|
|
3262
3262
|
return;
|
|
@@ -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;
|
|
@@ -62908,17 +63010,17 @@ function portInUseMessage(port2, servedBy) {
|
|
|
62908
63010
|
var VIEW_HANDOVER_KEY = "views.handover";
|
|
62909
63011
|
var VIEW_HANDOVER_MAX_AGE_MS = 10 * 6e4;
|
|
62910
63012
|
function recordViewHandover(settings, views2, inline, now = Date.now()) {
|
|
62911
|
-
const
|
|
63013
|
+
const open6 = views2.list().map((v) => {
|
|
62912
63014
|
const owner = inline?.owner(v.id);
|
|
62913
63015
|
return owner ? { ...v, inline: { sessionId: owner.sessionId, callId: owner.callId, tool: owner.tool } } : v;
|
|
62914
63016
|
});
|
|
62915
|
-
if (
|
|
63017
|
+
if (open6.length === 0) {
|
|
62916
63018
|
settings.deleteAppSetting(VIEW_HANDOVER_KEY);
|
|
62917
63019
|
return 0;
|
|
62918
63020
|
}
|
|
62919
|
-
const record2 = { at: now, views:
|
|
63021
|
+
const record2 = { at: now, views: open6 };
|
|
62920
63022
|
settings.setAppSetting(VIEW_HANDOVER_KEY, JSON.stringify(record2));
|
|
62921
|
-
return
|
|
63023
|
+
return open6.length;
|
|
62922
63024
|
}
|
|
62923
63025
|
function restoreViewHandover(settings, views2, inline, now = Date.now()) {
|
|
62924
63026
|
const raw = settings.appSetting(VIEW_HANDOVER_KEY);
|
|
@@ -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'",
|
|
@@ -63639,9 +63741,9 @@ import { join as join15 } from "node:path";
|
|
|
63639
63741
|
import { lstatSync as lstatSync2, watch } from "node:fs";
|
|
63640
63742
|
|
|
63641
63743
|
// packages/agent-host/src/dev-services/fs.ts
|
|
63642
|
-
import { spawn } from "node:child_process";
|
|
63744
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
63643
63745
|
import { constants as constants3 } from "node:fs";
|
|
63644
|
-
import { lstat as lstat2, mkdir, open, readdir, readlink, realpath as realpath2, rename, rm, stat as stat2, writeFile } from "node:fs/promises";
|
|
63746
|
+
import { lstat as lstat2, mkdir, open as open2, readdir, readlink, realpath as realpath2, rename, rm, stat as stat2, writeFile } from "node:fs/promises";
|
|
63645
63747
|
import { basename, dirname as dirname2, extname, isAbsolute as isAbsolute2, join as join5, relative as relative2, resolve, sep as sep2 } from "node:path";
|
|
63646
63748
|
|
|
63647
63749
|
// packages/agent-host/src/dev-services/path-guard.ts
|
|
@@ -63776,7 +63878,8 @@ function isMissingFsPath(error51) {
|
|
|
63776
63878
|
}
|
|
63777
63879
|
|
|
63778
63880
|
// packages/agent-host/src/dev-services/git-exec.ts
|
|
63779
|
-
import { execFile } from "node:child_process";
|
|
63881
|
+
import { execFile, spawn } from "node:child_process";
|
|
63882
|
+
import { open } from "node:fs/promises";
|
|
63780
63883
|
import { devNull } from "node:os";
|
|
63781
63884
|
import { promisify } from "node:util";
|
|
63782
63885
|
|
|
@@ -64002,6 +64105,40 @@ async function gitArgv(cwd, args, allowRepoPrograms = false) {
|
|
|
64002
64105
|
}
|
|
64003
64106
|
async function runGit(cwd, args, opts = {}) {
|
|
64004
64107
|
const argv = await gitArgv(cwd, args, opts.allowRepoPrograms);
|
|
64108
|
+
if (opts.outputFile) {
|
|
64109
|
+
const output = await open(opts.outputFile, "w", 384);
|
|
64110
|
+
try {
|
|
64111
|
+
await new Promise((resolve12, reject) => {
|
|
64112
|
+
const child = spawn(programPath("git"), argv, {
|
|
64113
|
+
cwd,
|
|
64114
|
+
stdio: ["pipe", output.fd, "pipe"],
|
|
64115
|
+
timeout: opts.timeout ?? DEFAULTS.timeout,
|
|
64116
|
+
...opts.env ? { env: { ...process.env, ...opts.env } } : {}
|
|
64117
|
+
});
|
|
64118
|
+
let stderr = "";
|
|
64119
|
+
let overflow = false;
|
|
64120
|
+
child.stderr.on("data", (chunk) => {
|
|
64121
|
+
if (overflow) return;
|
|
64122
|
+
stderr += chunk.toString();
|
|
64123
|
+
if (Buffer.byteLength(stderr) > (opts.maxBuffer ?? DEFAULTS.maxBuffer)) {
|
|
64124
|
+
overflow = true;
|
|
64125
|
+
child.kill();
|
|
64126
|
+
}
|
|
64127
|
+
});
|
|
64128
|
+
child.on("error", reject);
|
|
64129
|
+
child.on("close", (code2, signal) => {
|
|
64130
|
+
if (code2 === 0 && !overflow) resolve12();
|
|
64131
|
+
else reject(Object.assign(new Error(overflow ? "Git stderr exceeded maxBuffer" : stderr || `Git exited with ${code2 ?? signal}`), { code: code2, stderr }));
|
|
64132
|
+
});
|
|
64133
|
+
child.stdin.on("error", () => {
|
|
64134
|
+
});
|
|
64135
|
+
child.stdin.end(opts.input);
|
|
64136
|
+
});
|
|
64137
|
+
return "";
|
|
64138
|
+
} finally {
|
|
64139
|
+
await output.close();
|
|
64140
|
+
}
|
|
64141
|
+
}
|
|
64005
64142
|
const run = exec(programPath("git"), argv, {
|
|
64006
64143
|
cwd,
|
|
64007
64144
|
timeout: opts.timeout ?? DEFAULTS.timeout,
|
|
@@ -64052,6 +64189,13 @@ function installedCopyPath(platform = HOST_PLATFORM, env = process.env, home = h
|
|
|
64052
64189
|
}
|
|
64053
64190
|
return posix2.join(home, ".local/share/applications", `${APP_SLUG}.desktop`);
|
|
64054
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
|
+
}
|
|
64055
64199
|
|
|
64056
64200
|
// packages/agent-host/src/dev-services/fs.ts
|
|
64057
64201
|
var MAX_TEXT = 2e6;
|
|
@@ -64162,7 +64306,7 @@ async function readTextFile(root3, rel) {
|
|
|
64162
64306
|
const file2 = await resolveExisting(root3, rel);
|
|
64163
64307
|
const pathInfo = await stat2(file2, { bigint: true });
|
|
64164
64308
|
if (!pathInfo.isFile()) fail2("Path is not a regular file");
|
|
64165
|
-
const handle = await
|
|
64309
|
+
const handle = await open2(file2, READ_TEXT_FLAGS);
|
|
64166
64310
|
try {
|
|
64167
64311
|
const opened = await handle.stat({ bigint: true });
|
|
64168
64312
|
if (!opened.isFile()) fail2("Path is not a regular file");
|
|
@@ -64228,7 +64372,7 @@ async function copyTree(src, dst, clone2 = cloneTree) {
|
|
|
64228
64372
|
async function cloneTree(src, dst) {
|
|
64229
64373
|
if (!clonesFiles()) return false;
|
|
64230
64374
|
return new Promise((done) => {
|
|
64231
|
-
const p =
|
|
64375
|
+
const p = spawn2("/bin/cp", ["-Rc", src, dst], { stdio: "ignore" });
|
|
64232
64376
|
p.on("error", () => done(false));
|
|
64233
64377
|
p.on("close", (code2) => done(code2 === 0));
|
|
64234
64378
|
});
|
|
@@ -64385,7 +64529,7 @@ function isSymlink(path2) {
|
|
|
64385
64529
|
}
|
|
64386
64530
|
|
|
64387
64531
|
// packages/agent-host/src/apps/external/app-process.ts
|
|
64388
|
-
import { spawn as
|
|
64532
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
64389
64533
|
import { closeSync as closeSync2, existsSync as existsSync5, mkdirSync, openSync as openSync2, renameSync as renameSync3, statSync as statSync3, writeSync as writeSync3 } from "node:fs";
|
|
64390
64534
|
import { dirname as dirname3 } from "node:path";
|
|
64391
64535
|
|
|
@@ -79941,7 +80085,7 @@ var AppProcess = class _AppProcess {
|
|
|
79941
80085
|
log.note(`starting: ${spec.command} ${spec.args.join(" ")}`);
|
|
79942
80086
|
const launch2 = resolveCommand(spec.command, spec.env);
|
|
79943
80087
|
const spawnedAt = Date.now();
|
|
79944
|
-
const child =
|
|
80088
|
+
const child = spawn3(launch2.command, [...launch2.args, ...spec.args], {
|
|
79945
80089
|
cwd: spec.cwd,
|
|
79946
80090
|
env: spec.env,
|
|
79947
80091
|
/*
|
|
@@ -94045,7 +94189,7 @@ var RUN_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
|
94045
94189
|
var FAILURES_KEPT = 20;
|
|
94046
94190
|
var SUMMARY_CHARS = 200;
|
|
94047
94191
|
function announcingLedger(inner, announce) {
|
|
94048
|
-
const
|
|
94192
|
+
const open6 = /* @__PURE__ */ new Map();
|
|
94049
94193
|
const tell = (apps) => {
|
|
94050
94194
|
for (const a of apps ?? []) announce(a);
|
|
94051
94195
|
};
|
|
@@ -94054,20 +94198,20 @@ function announcingLedger(inner, announce) {
|
|
|
94054
94198
|
begin(row) {
|
|
94055
94199
|
inner.begin(row);
|
|
94056
94200
|
const apps = [{ projectId: row.projectId, appId: row.appId }];
|
|
94057
|
-
for (const up2 of row.parentRunId ?
|
|
94201
|
+
for (const up2 of row.parentRunId ? open6.get(row.parentRunId) ?? [] : []) {
|
|
94058
94202
|
if (!apps.some((a) => a.projectId === up2.projectId && a.appId === up2.appId)) apps.push(up2);
|
|
94059
94203
|
}
|
|
94060
|
-
|
|
94204
|
+
open6.set(row.id, apps);
|
|
94061
94205
|
tell(apps);
|
|
94062
94206
|
},
|
|
94063
94207
|
link(id2, sessionId) {
|
|
94064
94208
|
inner.link(id2, sessionId);
|
|
94065
|
-
tell(
|
|
94209
|
+
tell(open6.get(id2));
|
|
94066
94210
|
},
|
|
94067
94211
|
end(id2, e) {
|
|
94068
94212
|
inner.end(id2, e);
|
|
94069
|
-
tell(
|
|
94070
|
-
|
|
94213
|
+
tell(open6.get(id2));
|
|
94214
|
+
open6.delete(id2);
|
|
94071
94215
|
}
|
|
94072
94216
|
};
|
|
94073
94217
|
}
|
|
@@ -96600,8 +96744,8 @@ var ExternalApps = class {
|
|
|
96600
96744
|
return { runId, status, result, error: error51, durationMs };
|
|
96601
96745
|
};
|
|
96602
96746
|
if (!e.manifest) return done("rejected", null, `This app's manifest is invalid: ${e.error}`);
|
|
96603
|
-
const
|
|
96604
|
-
if (!e.scope.trusted && !
|
|
96747
|
+
const open6 = caller.kind === "session" && this.sessionBoundaryOpen(caller.sessionId);
|
|
96748
|
+
if (!e.scope.trusted && !open6) return done("rejected", null, "This app's project is not trusted, so Centralu does not call its apps");
|
|
96605
96749
|
const held3 = this.held(e);
|
|
96606
96750
|
if (held3) return done("rejected", null, held3);
|
|
96607
96751
|
if (e.life.gaveUp) return done("rejected", null, `This app stopped after failing ${this.timing.maxFailures} times in a row`);
|
|
@@ -96649,7 +96793,7 @@ var ExternalApps = class {
|
|
|
96649
96793
|
abort.abort();
|
|
96650
96794
|
for (const sig of upstream) sig.removeEventListener("abort", onUp);
|
|
96651
96795
|
}
|
|
96652
|
-
},
|
|
96796
|
+
}, open6);
|
|
96653
96797
|
} catch (err) {
|
|
96654
96798
|
return done("error", null, err.message);
|
|
96655
96799
|
}
|
|
@@ -97960,9 +98104,9 @@ var InlineViews = class {
|
|
|
97960
98104
|
* just opened is never the one closed.
|
|
97961
98105
|
*/
|
|
97962
98106
|
capLive(sessionId, keep) {
|
|
97963
|
-
const
|
|
98107
|
+
const open6 = [...this.bySession.get(sessionId)?.values() ?? []].filter((x) => x.instanceId).sort((a, b2) => a.openedAt - b2.openedAt);
|
|
97964
98108
|
const n = this.limits.livePerSession;
|
|
97965
|
-
for (const old of
|
|
98109
|
+
for (const old of open6.slice(0, Math.max(0, open6.length - n))) {
|
|
97966
98110
|
if (old === keep) continue;
|
|
97967
98111
|
this.shut(old);
|
|
97968
98112
|
this.deps.emit({
|
|
@@ -98172,8 +98316,9 @@ var OriginPorts = class {
|
|
|
98172
98316
|
|
|
98173
98317
|
// packages/agent-host/src/dev-services/worktree-recovery.ts
|
|
98174
98318
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
98175
|
-
import { existsSync as existsSync13 } from "node:fs";
|
|
98176
|
-
import { chmod, lstat as lstat3, mkdir as mkdir2, readFile, readdir as readdir2, realpath as realpath3, rm as rm2, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
|
|
98319
|
+
import { createReadStream, existsSync as existsSync13 } from "node:fs";
|
|
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";
|
|
98321
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
98177
98322
|
import { join as join16, resolve as resolve2 } from "node:path";
|
|
98178
98323
|
var WORKTREE_RECOVERY_MAX_BYTES = 256 * 1024 * 1024;
|
|
98179
98324
|
var RECOVERY_TIMEOUT = 12e4;
|
|
@@ -98181,8 +98326,70 @@ function worktreeRecoveryDir(sessionId) {
|
|
|
98181
98326
|
if (!isSessionId(sessionId)) throw new Error("Invalid session id for worktree recovery");
|
|
98182
98327
|
return join16(dataRoot(), "worktree-recovery", sessionId);
|
|
98183
98328
|
}
|
|
98184
|
-
async function
|
|
98185
|
-
|
|
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}`]);
|
|
98374
|
+
}
|
|
98375
|
+
async function* gitRecords(repo, args, input) {
|
|
98376
|
+
const dir = await mkdtemp(join16(tmpdir2(), "cc-recovery-git-"));
|
|
98377
|
+
const file2 = join16(dir, "records");
|
|
98378
|
+
try {
|
|
98379
|
+
await runGit(repo, args, { outputFile: file2, timeout: RECOVERY_TIMEOUT, input });
|
|
98380
|
+
let pending = "";
|
|
98381
|
+
for await (const chunk of createReadStream(file2, { encoding: "utf8" })) {
|
|
98382
|
+
pending += chunk;
|
|
98383
|
+
let end;
|
|
98384
|
+
while ((end = pending.indexOf("\0")) !== -1) {
|
|
98385
|
+
yield pending.slice(0, end);
|
|
98386
|
+
pending = pending.slice(end + 1);
|
|
98387
|
+
}
|
|
98388
|
+
}
|
|
98389
|
+
if (pending) yield pending;
|
|
98390
|
+
} finally {
|
|
98391
|
+
await rm2(dir, { recursive: true, force: true });
|
|
98392
|
+
}
|
|
98186
98393
|
}
|
|
98187
98394
|
async function saveWorktreeRecovery(sessionId, cwd, trunk, branch) {
|
|
98188
98395
|
const plans = [];
|
|
@@ -98193,7 +98400,7 @@ async function saveWorktreeRecovery(sessionId, cwd, trunk, branch) {
|
|
|
98193
98400
|
throw new Error(`Worktree recovery exceeds the 256 MiB input limit (tracked and non-ignored untracked files plus staged changes, including submodules). No files were staged or removed; save the work elsewhere before deleting.`);
|
|
98194
98401
|
}
|
|
98195
98402
|
};
|
|
98196
|
-
const prepare = async (repo, path2) => {
|
|
98403
|
+
const prepare = async (repo, path2, gitlinks = []) => {
|
|
98197
98404
|
const tip = (await runGit(repo, ["rev-parse", "--verify", "HEAD"])).trim();
|
|
98198
98405
|
const refs = ["HEAD"];
|
|
98199
98406
|
if (!path2 && branch) {
|
|
@@ -98201,20 +98408,85 @@ async function saveWorktreeRecovery(sessionId, cwd, trunk, branch) {
|
|
|
98201
98408
|
if (await runGit(repo, ["rev-parse", "--verify", ref]).then(() => true).catch(() => false)) refs.push(ref);
|
|
98202
98409
|
} else if (path2) {
|
|
98203
98410
|
refs.push(...(await runGit(repo, ["for-each-ref", "--format=%(refname)", "refs/heads/"])).trim().split("\n").filter(Boolean));
|
|
98411
|
+
if (gitlinks.length) {
|
|
98412
|
+
const available = await runGit(repo, ["cat-file", "--batch-check=%(objectname) %(objecttype)"], {
|
|
98413
|
+
input: gitlinks.map((oid) => `${oid}^{commit}`).join("\n") + "\n",
|
|
98414
|
+
timeout: RECOVERY_TIMEOUT,
|
|
98415
|
+
// Older Git ignores the lazy-fetch switch; disallow transports there too.
|
|
98416
|
+
env: { GIT_NO_LAZY_FETCH: "1", GIT_ALLOW_PROTOCOL: "" }
|
|
98417
|
+
});
|
|
98418
|
+
for (const row of available.trim().split("\n")) {
|
|
98419
|
+
const [oid, type] = row.split(" ");
|
|
98420
|
+
if (type === "commit") refs.push(oid);
|
|
98421
|
+
}
|
|
98422
|
+
}
|
|
98204
98423
|
}
|
|
98205
98424
|
const baselineOid = !path2 ? await runGit(repo, ["rev-parse", "--verify", `${trunk}^{commit}`]).then((s) => s.trim()).catch(() => null) : null;
|
|
98206
|
-
const exclude = path2 ? ["--
|
|
98207
|
-
const
|
|
98425
|
+
const exclude = path2 ? (await runGit(repo, ["for-each-ref", "--format=^%(objectname)", "refs/remotes/"])).trim().split("\n").filter(Boolean) : baselineOid ? [`^${baselineOid}`] : [];
|
|
98426
|
+
const revisions = [...refs, ...exclude].join("\n") + "\n";
|
|
98427
|
+
const commits = Number((await runGit(repo, ["rev-list", "--count", "--stdin"], { input: revisions, timeout: RECOVERY_TIMEOUT })).trim());
|
|
98208
98428
|
plans.push({ repo, path: path2, tip, refs, commits });
|
|
98209
98429
|
const stagedNames = new Set((await runGit(repo, ["diff", "--cached", "--name-only", "-z", "--no-renames", tip], { timeout: RECOVERY_TIMEOUT })).split("\0"));
|
|
98210
98430
|
const modules = [];
|
|
98431
|
+
const moduleCommits = /* @__PURE__ */ new Map();
|
|
98432
|
+
const keepGitlink = (rel, oid) => {
|
|
98433
|
+
modules.push(rel);
|
|
98434
|
+
const tips = moduleCommits.get(rel) ?? /* @__PURE__ */ new Set();
|
|
98435
|
+
tips.add(oid);
|
|
98436
|
+
moduleCommits.set(rel, tips);
|
|
98437
|
+
};
|
|
98438
|
+
const manifests = /* @__PURE__ */ new Set();
|
|
98439
|
+
const keepManifest = (rel, oid) => {
|
|
98440
|
+
if (rel === ".gitmodules" && !/^0+$/.test(oid)) manifests.add(oid);
|
|
98441
|
+
};
|
|
98442
|
+
let change = null;
|
|
98443
|
+
for await (const row of gitRecords(repo, ["log", "--raw", "-z", "--no-renames", "--ignore-submodules=none", "--no-abbrev", "--format=", "--root", "--diff-merges=separate", "--stdin"], revisions)) {
|
|
98444
|
+
if (change) {
|
|
98445
|
+
const [, oldMode, newMode, oldOid, newOid] = change;
|
|
98446
|
+
if (oldMode === "160000") keepGitlink(row, oldOid);
|
|
98447
|
+
if (newMode === "160000") keepGitlink(row, newOid);
|
|
98448
|
+
keepManifest(row, oldOid);
|
|
98449
|
+
keepManifest(row, newOid);
|
|
98450
|
+
change = null;
|
|
98451
|
+
} else change = row.match(/^\n*:(\d{6}) (\d{6}) ([a-f0-9]+) ([a-f0-9]+) [A-Z]$/);
|
|
98452
|
+
}
|
|
98453
|
+
for await (const row of gitRecords(repo, ["ls-tree", "-r", "-z", "HEAD"])) {
|
|
98454
|
+
const tab = row.indexOf(" ");
|
|
98455
|
+
const [mode, , oid] = row.slice(0, tab).split(" ");
|
|
98456
|
+
const rel = row.slice(tab + 1);
|
|
98457
|
+
if (mode === "160000") keepGitlink(rel, oid);
|
|
98458
|
+
keepManifest(rel, oid);
|
|
98459
|
+
}
|
|
98211
98460
|
const stagedObjects = [];
|
|
98212
|
-
for (const row of (
|
|
98461
|
+
for await (const row of gitRecords(repo, ["ls-files", "--stage", "-z"])) {
|
|
98213
98462
|
if (!row) continue;
|
|
98214
98463
|
const tab = row.indexOf(" ");
|
|
98215
98464
|
const rel = row.slice(tab + 1);
|
|
98216
|
-
|
|
98217
|
-
|
|
98465
|
+
const oid = row.split(" ")[1];
|
|
98466
|
+
if (row.startsWith("160000 ")) keepGitlink(rel, oid);
|
|
98467
|
+
else if (stagedNames.has(rel)) stagedObjects.push(oid);
|
|
98468
|
+
keepManifest(rel, oid);
|
|
98469
|
+
}
|
|
98470
|
+
const modulePaths = /* @__PURE__ */ new Map();
|
|
98471
|
+
for (const oid of manifests) {
|
|
98472
|
+
const config2 = await runGit(repo, ["config", "--blob", oid, "-z", "--get-regexp", "^submodule\\..*\\.path$"]).catch((e) => {
|
|
98473
|
+
if (e.code === 1) return "";
|
|
98474
|
+
throw e;
|
|
98475
|
+
});
|
|
98476
|
+
for (const row of config2.split("\0").filter(Boolean)) {
|
|
98477
|
+
const newline = row.indexOf("\n");
|
|
98478
|
+
const name = row.slice("submodule.".length, newline - ".path".length);
|
|
98479
|
+
const paths = modulePaths.get(name) ?? /* @__PURE__ */ new Set();
|
|
98480
|
+
paths.add(row.slice(newline + 1));
|
|
98481
|
+
modulePaths.set(name, paths);
|
|
98482
|
+
}
|
|
98483
|
+
}
|
|
98484
|
+
for (const paths of modulePaths.values()) {
|
|
98485
|
+
const tips = new Set([...paths].flatMap((rel) => [...moduleCommits.get(rel) ?? []]));
|
|
98486
|
+
for (const rel of paths) {
|
|
98487
|
+
modules.push(rel);
|
|
98488
|
+
moduleCommits.set(rel, /* @__PURE__ */ new Set([...moduleCommits.get(rel) ?? [], ...tips]));
|
|
98489
|
+
}
|
|
98218
98490
|
}
|
|
98219
98491
|
if (stagedObjects.length) {
|
|
98220
98492
|
const sizes = await runGit(repo, ["cat-file", "--batch-check=%(objectsize)"], { input: stagedObjects.join("\n") + "\n" });
|
|
@@ -98236,7 +98508,7 @@ async function saveWorktreeRecovery(sessionId, cwd, trunk, branch) {
|
|
|
98236
98508
|
await assertExistingPath(repo, rel).catch((e) => {
|
|
98237
98509
|
if (existsSync13(join16(repo, rel))) throw e;
|
|
98238
98510
|
});
|
|
98239
|
-
if (existsSync13(join16(repo, rel, ".git"))) await prepare(join16(repo, rel), path2 ? `${path2}/${rel}` : rel);
|
|
98511
|
+
if (existsSync13(join16(repo, rel, ".git"))) await prepare(join16(repo, rel), path2 ? `${path2}/${rel}` : rel, [...moduleCommits.get(rel) ?? []]);
|
|
98240
98512
|
}
|
|
98241
98513
|
};
|
|
98242
98514
|
await prepare(cwd, "");
|
|
@@ -98257,7 +98529,26 @@ async function saveWorktreeRecovery(sessionId, cwd, trunk, branch) {
|
|
|
98257
98529
|
const entry = { path: path2, tip, refs, patch: false, staged: false, bundle: commits > 0 };
|
|
98258
98530
|
snapshot2.repositories.push(entry);
|
|
98259
98531
|
snapshot2.commits += commits;
|
|
98260
|
-
if (entry.bundle)
|
|
98532
|
+
if (entry.bundle) {
|
|
98533
|
+
const temporaryRefs = [];
|
|
98534
|
+
const updates2 = [];
|
|
98535
|
+
try {
|
|
98536
|
+
entry.refs = [];
|
|
98537
|
+
for (const ref of refs) {
|
|
98538
|
+
if (ref === "HEAD" || ref.startsWith("refs/")) entry.refs.push(ref);
|
|
98539
|
+
else {
|
|
98540
|
+
const name = `refs/centralu-recovery/${snapshotId}/gitlink-${temporaryRefs.length}`;
|
|
98541
|
+
temporaryRefs.push(name);
|
|
98542
|
+
updates2.push(`create ${name} ${ref}`);
|
|
98543
|
+
entry.refs.push(name);
|
|
98544
|
+
}
|
|
98545
|
+
}
|
|
98546
|
+
if (updates2.length) await runGit(repo, ["update-ref", "--stdin"], { input: updates2.join("\n") + "\n", timeout: RECOVERY_TIMEOUT });
|
|
98547
|
+
await runGit(repo, ["bundle", "create", join16(dir, `${n}.bundle`), "--stdin"], { input: entry.refs.join("\n") + "\n", timeout: RECOVERY_TIMEOUT });
|
|
98548
|
+
} finally {
|
|
98549
|
+
if (temporaryRefs.length) await runGit(repo, ["update-ref", "--stdin"], { input: temporaryRefs.map((ref) => `delete ${ref}`).join("\n") + "\n", timeout: RECOVERY_TIMEOUT });
|
|
98550
|
+
}
|
|
98551
|
+
}
|
|
98261
98552
|
entry.staged = await patchFile(repo, tip, join16(dir, `${n}.staged.patch`));
|
|
98262
98553
|
const stagedNames = await runGit(repo, ["diff", "--cached", "--name-only", "-z", "--no-renames", tip], { timeout: RECOVERY_TIMEOUT });
|
|
98263
98554
|
const env = { GIT_INDEX_FILE: join16(dir, `${n}.index`) };
|
|
@@ -98292,28 +98583,83 @@ async function restoreWorktreeRecovery(sessionId, repo, path2, branch, recovery,
|
|
|
98292
98583
|
const dir = join16(worktreeRecoveryDir(sessionId), recovery.snapshot);
|
|
98293
98584
|
const snapshot2 = JSON.parse(await readFile(join16(dir, "snapshot.json"), "utf8"));
|
|
98294
98585
|
const root3 = snapshot2.repositories[0];
|
|
98586
|
+
let extraNotice = "";
|
|
98587
|
+
if (root3.bundle) {
|
|
98588
|
+
const heads = (await runGit(repo, ["bundle", "list-heads", join16(dir, "0.bundle")])).trim().split("\n");
|
|
98589
|
+
for (const row of heads) {
|
|
98590
|
+
const [tip, ref] = row.split(" ");
|
|
98591
|
+
if (!tip || !ref?.startsWith("refs/heads/") || tip === recovery.tip) continue;
|
|
98592
|
+
const base2 = `recovered/${sessionId}-stored-branch`;
|
|
98593
|
+
let recovered = base2;
|
|
98594
|
+
for (let suffix = 1; ; suffix++) {
|
|
98595
|
+
const existing = await runGit(repo, ["rev-parse", "--verify", `refs/heads/${recovered}`]).then((s) => s.trim()).catch(() => null);
|
|
98596
|
+
if (existing === tip) break;
|
|
98597
|
+
if (!existing) {
|
|
98598
|
+
await runGit(repo, ["fetch", join16(dir, "0.bundle"), `${ref}:refs/heads/${recovered}`], { allowRepoPrograms: trust.trusted === true });
|
|
98599
|
+
break;
|
|
98600
|
+
}
|
|
98601
|
+
recovered = `${base2}-${suffix}`;
|
|
98602
|
+
}
|
|
98603
|
+
extraNotice += `Stored branch ${ref.slice("refs/heads/".length)} recovered as ${recovered}. `;
|
|
98604
|
+
}
|
|
98605
|
+
}
|
|
98295
98606
|
if (!existsSync13(path2)) {
|
|
98296
|
-
|
|
98297
|
-
|
|
98607
|
+
await rm2(join16(dir, "patches-applied"), { force: true });
|
|
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
|
+
}
|
|
98298
98630
|
}
|
|
98299
98631
|
if (root3.bundle) await runGit(repo, ["fetch", join16(dir, "0.bundle"), ...root3.refs ?? ["HEAD"]], { allowRepoPrograms: trust.trusted === true });
|
|
98300
|
-
|
|
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 });
|
|
98301
98639
|
if (root3.patch || root3.staged) {
|
|
98302
98640
|
try {
|
|
98303
98641
|
if (root3.staged) await runGit(path2, ["apply", "--cached", "--binary", join16(dir, "0.staged.patch")]);
|
|
98304
98642
|
if (root3.patch) await runGit(path2, ["apply", "--binary", join16(dir, "0.patch")]);
|
|
98643
|
+
await writeFile2(join16(dir, "patches-applied"), await realpath3(path2), { mode: 384 });
|
|
98305
98644
|
} catch {
|
|
98306
|
-
return { branch, notice: `Restored branch ${branch}. Patches could not all be applied. Recovery files remain at ${dir}; apply numbered .patch files manually with git apply and .staged.patch files with git apply --cached.` };
|
|
98645
|
+
return { branch, notice: `Restored branch ${branch}. ${extraNotice}Patches could not all be applied. Recovery files remain at ${dir}; apply numbered .patch files manually with git apply and .staged.patch files with git apply --cached.` };
|
|
98307
98646
|
}
|
|
98308
98647
|
}
|
|
98309
98648
|
} else {
|
|
98310
98649
|
const common = async (cwd) => realpath3(resolve2(cwd, (await runGit(cwd, ["rev-parse", "--git-common-dir"])).trim()));
|
|
98311
98650
|
if (await common(path2) !== await common(repo)) throw new Error(`The worktree path belongs to another repository. Recovery files remain at ${dir}`);
|
|
98312
98651
|
branch = (await runGit(path2, ["symbolic-ref", "--quiet", "--short", "HEAD"])).trim();
|
|
98652
|
+
if (root3.patch || root3.staged) {
|
|
98653
|
+
const appliedTo = await readFile(join16(dir, "patches-applied"), "utf8").catch((e) => {
|
|
98654
|
+
if (e.code === "ENOENT") return null;
|
|
98655
|
+
throw e;
|
|
98656
|
+
});
|
|
98657
|
+
extraNotice += appliedTo === await realpath3(path2) ? "Saved patches were applied during the previous restore attempt; they were not reapplied on this retry. " : "The worktree already existed; saved patches were not applied on this retry and may still need manual recovery. ";
|
|
98658
|
+
}
|
|
98313
98659
|
}
|
|
98314
98660
|
return {
|
|
98315
98661
|
branch,
|
|
98316
|
-
notice: `Restored branch ${branch}. Recovery files remain at ${dir}: ${recovery.changedFiles} uncommitted files, ${recovery.commits} commits absent from the trunk or submodule remotes. Numbered .patch files can be applied with git apply; .bundle files list their saved refs with git bundle list-heads and can be fetched with git fetch <bundle> <ref>:<branch>. snapshot.json names each repository and its refs. Submodule recovery is manual. These files are removed when this session is deleted for good.`
|
|
98662
|
+
notice: `Restored branch ${branch}. ${extraNotice}Recovery files remain at ${dir}: ${recovery.changedFiles} uncommitted files, ${recovery.commits} commits absent from the trunk or submodule remotes. Numbered .patch files can be applied with git apply; .bundle files list their saved refs with git bundle list-heads and can be fetched with git fetch <bundle> <ref>:<branch>. snapshot.json names each repository and its refs. Submodule recovery is manual. These files are removed when this session is deleted for good.`
|
|
98317
98663
|
};
|
|
98318
98664
|
}
|
|
98319
98665
|
|
|
@@ -98406,13 +98752,23 @@ and approved skills, and whether every session can look at its own project), App
|
|
|
98406
98752
|
Shortcuts (the shortcut list), and Updates.`,
|
|
98407
98753
|
updates: `# Updates
|
|
98408
98754
|
Check from Settings \u2192 Updates. It reports a new version based on the npm registry, and the person
|
|
98409
|
-
clicks to install it.
|
|
98410
|
-
|
|
98755
|
+
clicks to install it. With automatic checking on, the check runs on startup, every six hours, when
|
|
98756
|
+
Updates opens, and on focus when the last check is over an hour old. With it off, use Check now.
|
|
98757
|
+
Ordinary checks follow npm, including dist-tag rollbacks. Installing reuses a successful check
|
|
98758
|
+
from the last minute or checks again. Only while installing can a higher stored version seen less
|
|
98759
|
+
than an hour ago win over the fresh tag. Older observations yield to npm. If the check fails, it
|
|
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.
|
|
98411
98764
|
Once installed, the desktop app offers "Apply now": it relaunches into the new version, and running
|
|
98412
98765
|
agents, terminals and commands keep going. "Apply updates automatically when idle" (off by default)
|
|
98413
98766
|
installs a new version without a click and applies it once no session is working or waiting, no
|
|
98414
|
-
terminal or command is running, and the person is not typing.
|
|
98415
|
-
|
|
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.`
|
|
98416
98772
|
};
|
|
98417
98773
|
function gist(description) {
|
|
98418
98774
|
const first = description.split(/ — |\. /)[0] ?? description;
|
|
@@ -98471,8 +98827,8 @@ function trustedJsonText(value) {
|
|
|
98471
98827
|
var ORCHESTRATOR_TOOLS = [
|
|
98472
98828
|
{
|
|
98473
98829
|
name: "update_centralu",
|
|
98474
|
-
description: "Checks for or installs a Centralu update.
|
|
98475
|
-
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"]) })
|
|
98476
98832
|
},
|
|
98477
98833
|
{
|
|
98478
98834
|
name: "list_sessions",
|
|
@@ -98610,8 +98966,9 @@ var BUILDER_TOOL_NAMES = ["check"];
|
|
|
98610
98966
|
var READER_TOOLS = [
|
|
98611
98967
|
{
|
|
98612
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
|
|
98613
98970
|
description: "Updates Centralu with consent.",
|
|
98614
|
-
schema: external_exports.object({ action: external_exports.enum(["check", "install"]) }),
|
|
98971
|
+
schema: external_exports.object({ action: external_exports.enum(["check", "install", "apply"]) }),
|
|
98615
98972
|
// Updates are rare (#581); keep the definition behind Claude's tool search like app_guide.
|
|
98616
98973
|
deferred: true
|
|
98617
98974
|
},
|
|
@@ -98771,6 +99128,11 @@ function askProjectText(r) {
|
|
|
98771
99128
|
}
|
|
98772
99129
|
function agentRunText(r) {
|
|
98773
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
|
+
}
|
|
98774
99136
|
const who = `the ${r.tool} session "${r.sessionName}" [${r.sessionId}]`;
|
|
98775
99137
|
switch (r.state) {
|
|
98776
99138
|
case "working":
|
|
@@ -98778,8 +99140,10 @@ function agentRunText(r) {
|
|
|
98778
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.`
|
|
98779
99141
|
};
|
|
98780
99142
|
case "stopped":
|
|
99143
|
+
if (!r.sessionName) return { text: `Withdrew request ${r.sessionId}: its card is gone and no session was started.` };
|
|
98781
99144
|
return { text: `Stopped ${who}.` };
|
|
98782
99145
|
case "deleted":
|
|
99146
|
+
if (r.branch === void 0) return { text: `Moved ${who} to the trash. Tell the person in one line.` };
|
|
98783
99147
|
return {
|
|
98784
99148
|
text: (r.warning ? `Moved ${who} to the trash. Warning: ${r.warning} ` : `Cleaned up ${who}: its worktree and branch "${r.branch}" were removed and the session went to the trash. `) + (r.setAside.changedFiles || r.setAside.commits ? `${r.setAside.changedFiles} uncommitted files, ${r.setAside.commits} commits not on the trunk, kept in the trash. ` : "Nothing needed setting aside. ") + "Tell the person in one line."
|
|
98785
99149
|
};
|
|
@@ -98802,13 +99166,25 @@ agent_session(sessionId="${r.sessionId}", action="send", text=\u2026) gives it m
|
|
|
98802
99166
|
async function runOrchestratorTool(tools, name, args, caller = { sessionId: null, profile: "human" }, signal) {
|
|
98803
99167
|
if (name === "update_centralu") {
|
|
98804
99168
|
const action = args.action;
|
|
98805
|
-
if (action !== "check" && action !== "install") return { text: "Give action: check or
|
|
98806
|
-
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
|
+
}
|
|
98807
99178
|
if (!r.ok) return { text: `Centralu update did not start: ${r.error ?? "unavailable"}`, isError: true };
|
|
98808
99179
|
const s = r.status;
|
|
98809
99180
|
if (!s) return { text: "Centralu update is unavailable.", isError: true };
|
|
98810
99181
|
if (action === "check") return { text: s.newer && s.latest ? `Centralu ${s.latest} is available.` : s.error ?? "Centralu is up to date." };
|
|
98811
|
-
|
|
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." };
|
|
98812
99188
|
}
|
|
98813
99189
|
if (name === "list_sessions") return listSessionsText(tools, caller);
|
|
98814
99190
|
if (name === "centralu_commands" || name === "centralu_help" || name === "centralu_run") return runCommandTool(tools, name, args, caller, signal);
|
|
@@ -100988,8 +101364,8 @@ function orchestratorMemory(reader, sessionId) {
|
|
|
100988
101364
|
|
|
100989
101365
|
// packages/agent-host/src/sessions/manager.ts
|
|
100990
101366
|
import { homedir as homedir12 } from "node:os";
|
|
100991
|
-
import { basename as basename5, isAbsolute as isAbsolute9, join as join29, relative as relative8, resolve as resolve11, sep as sep11 } from "node:path";
|
|
100992
|
-
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";
|
|
100993
101369
|
import { exec as exec6 } from "node:child_process";
|
|
100994
101370
|
|
|
100995
101371
|
// packages/agent-host/src/adapters/claude/index.ts
|
|
@@ -101604,7 +101980,7 @@ async function readClaudeModels(q2) {
|
|
|
101604
101980
|
}
|
|
101605
101981
|
|
|
101606
101982
|
// packages/agent-host/src/adapters/local-process.ts
|
|
101607
|
-
import { spawn as
|
|
101983
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
101608
101984
|
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
101609
101985
|
var EXIT_AFTER_OUTPUT_MS3 = 1e3;
|
|
101610
101986
|
var STDERR_TAIL = 8 * 1024;
|
|
@@ -101634,7 +102010,7 @@ var LocalAgentProcess = class _LocalAgentProcess extends EventEmitter3 {
|
|
|
101634
102010
|
exitReported = false;
|
|
101635
102011
|
spawnedAt = Date.now();
|
|
101636
102012
|
static spawn(spec, os2) {
|
|
101637
|
-
const child =
|
|
102013
|
+
const child = spawn5(spec.command, spec.args, {
|
|
101638
102014
|
cwd: spec.cwd,
|
|
101639
102015
|
env: spec.env,
|
|
101640
102016
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -102192,6 +102568,28 @@ var ClaudeBackgroundTracker = class {
|
|
|
102192
102568
|
recover(messages2) {
|
|
102193
102569
|
return this.schedules.recover(messages2) ? [this.event()] : [];
|
|
102194
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
|
+
}
|
|
102195
102593
|
/** Drops the wake-ups whose time has passed; the new level when that changed anything */
|
|
102196
102594
|
tick() {
|
|
102197
102595
|
return this.schedules.prune() ? [this.event()] : [];
|
|
@@ -102219,7 +102617,7 @@ var ClaudeBackgroundTracker = class {
|
|
|
102219
102617
|
this.live.push(id2);
|
|
102220
102618
|
this.seen.add(id2);
|
|
102221
102619
|
}
|
|
102222
|
-
return [this.event()];
|
|
102620
|
+
return [this.event(void 0, true)];
|
|
102223
102621
|
}
|
|
102224
102622
|
if (subtype === "task_started" || subtype === "task_updated") {
|
|
102225
102623
|
const id2 = str4(m.task_id);
|
|
@@ -102285,9 +102683,10 @@ var ClaudeBackgroundTracker = class {
|
|
|
102285
102683
|
status: "running"
|
|
102286
102684
|
};
|
|
102287
102685
|
}
|
|
102288
|
-
event(ended) {
|
|
102686
|
+
event(ended, snapshot2) {
|
|
102289
102687
|
return {
|
|
102290
102688
|
type: "background_tasks",
|
|
102689
|
+
...snapshot2 ? { snapshot: snapshot2 } : {},
|
|
102291
102690
|
sessionId: this.sessionId,
|
|
102292
102691
|
live: [...this.live.map((id2) => this.entry(id2, this.info.get(id2))), ...this.schedules.live()],
|
|
102293
102692
|
...ended ? { ended } : {}
|
|
@@ -102818,10 +103217,19 @@ function readTarget(toolName, input) {
|
|
|
102818
103217
|
const p = toolName === "Read" ? input.file_path : input.path;
|
|
102819
103218
|
return typeof p === "string" && p ? p : null;
|
|
102820
103219
|
}
|
|
102821
|
-
function permissionOptionsFor(
|
|
102822
|
-
if (
|
|
102823
|
-
if (
|
|
102824
|
-
return {
|
|
103220
|
+
async function permissionOptionsFor(opts) {
|
|
103221
|
+
if (opts.permissionPreset === "auto") return { permissionMode: "bypassPermissions" };
|
|
103222
|
+
if (opts.permissionPreset === "safe") return { permissionMode: "default" };
|
|
103223
|
+
if (opts.noSettingFiles) return { permissionMode: await personsDefaultMode() ?? "default" };
|
|
103224
|
+
try {
|
|
103225
|
+
const resolved = await D9t({ cwd: opts.cwd, ...settingSourcesFor(opts) });
|
|
103226
|
+
const mode = qtt(resolved).permissions?.defaultMode;
|
|
103227
|
+
if (typeof mode === "string" && PERMISSION_MODES.includes(mode)) {
|
|
103228
|
+
return { permissionMode: mode };
|
|
103229
|
+
}
|
|
103230
|
+
} catch {
|
|
103231
|
+
}
|
|
103232
|
+
return { permissionMode: "default" };
|
|
102825
103233
|
}
|
|
102826
103234
|
var PERMISSION_MODES = ["default", "acceptEdits", "bypassPermissions", "plan", "dontAsk", "auto"];
|
|
102827
103235
|
async function personsDefaultMode() {
|
|
@@ -102865,6 +103273,7 @@ var ClaudeSession = class {
|
|
|
102865
103273
|
this.stream.goal.seed(opts.knownGoal);
|
|
102866
103274
|
if (opts.processSource?.adopt) this.externalId = opts.resumeExternalId ?? null;
|
|
102867
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);
|
|
102868
103277
|
}
|
|
102869
103278
|
sessionId;
|
|
102870
103279
|
opts;
|
|
@@ -102961,7 +103370,7 @@ var ClaudeSession = class {
|
|
|
102961
103370
|
this.orchestratorServer = orchestratorMcp(this.opts.orchestratorTools, this.opts.toolProfile, this.opts.sessionId);
|
|
102962
103371
|
}
|
|
102963
103372
|
const servers = this.mcpServers();
|
|
102964
|
-
const
|
|
103373
|
+
const permissionOptions = await permissionOptionsFor(this.opts);
|
|
102965
103374
|
if (!this.opts.processSource?.adopt) await this.launch.gate.turn();
|
|
102966
103375
|
this.runKey = run?.key ?? null;
|
|
102967
103376
|
this.launch.links.acquire(this.runKey);
|
|
@@ -103054,7 +103463,7 @@ var ClaudeSession = class {
|
|
|
103054
103463
|
*/
|
|
103055
103464
|
...this.opts.systemPromptAppend ? { systemPrompt: { type: "preset", preset: "claude_code", append: this.opts.systemPromptAppend } } : {}
|
|
103056
103465
|
} : {},
|
|
103057
|
-
...
|
|
103466
|
+
...permissionOptions,
|
|
103058
103467
|
/*
|
|
103059
103468
|
* The agent an app hands a schema and asks for output (M4 D-1). It is a per-query option,
|
|
103060
103469
|
* so it can only be given when the session starts. Measured (SDK 0.3.263, CLI 2.1.282,
|
|
@@ -103296,8 +103705,8 @@ var ClaudeSession = class {
|
|
|
103296
103705
|
* turn released, rather than left hanging; the person can send the message again.
|
|
103297
103706
|
*/
|
|
103298
103707
|
releaseLostCalls(q2) {
|
|
103299
|
-
const
|
|
103300
|
-
const lost =
|
|
103708
|
+
const open6 = this.opts.processSource?.adopt?.openCalls ?? [];
|
|
103709
|
+
const lost = open6.filter((c) => isOrchestratorTool(c.tool) || appToolOf(c.tool) !== null);
|
|
103301
103710
|
if (lost.length === 0) return;
|
|
103302
103711
|
this.turnOpen = true;
|
|
103303
103712
|
this.stream.stopped();
|
|
@@ -103428,11 +103837,11 @@ var ClaudeSession = class {
|
|
|
103428
103837
|
* the model receives the answers in the CLI's own words (see the AskUserQuestion branch of `canUseTool`).
|
|
103429
103838
|
*/
|
|
103430
103839
|
answerQuestion(requestId, answers) {
|
|
103431
|
-
const
|
|
103432
|
-
if (!
|
|
103840
|
+
const open6 = this.questions.get(requestId);
|
|
103841
|
+
if (!open6) return false;
|
|
103433
103842
|
this.questions.delete(requestId);
|
|
103434
103843
|
const byQuestion = Object.fromEntries(answers.map((a) => [a.question, a.answers.join(", ")]));
|
|
103435
|
-
|
|
103844
|
+
open6.resolve({ behavior: "allow", updatedInput: { ...open6.input, answers: byQuestion } });
|
|
103436
103845
|
this.emit({ type: "question_resolved", sessionId: this.sessionId, requestId });
|
|
103437
103846
|
return true;
|
|
103438
103847
|
}
|
|
@@ -103708,8 +104117,8 @@ var ClaudeSession = class {
|
|
|
103708
104117
|
}
|
|
103709
104118
|
/** Releases a choice that was still waiting on an answer — **never released silently**, for the same reason as approval. */
|
|
103710
104119
|
releaseQuestions(why) {
|
|
103711
|
-
for (const [id2,
|
|
103712
|
-
|
|
104120
|
+
for (const [id2, open6] of this.questions) {
|
|
104121
|
+
open6.resolve({ behavior: "deny", message: why });
|
|
103713
104122
|
this.emit({ type: "question_resolved", sessionId: this.sessionId, requestId: id2 });
|
|
103714
104123
|
}
|
|
103715
104124
|
this.questions.clear();
|
|
@@ -103815,7 +104224,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
|
|
|
103815
104224
|
sleep: true
|
|
103816
104225
|
};
|
|
103817
104226
|
launchSettingsFingerprint(settings) {
|
|
103818
|
-
return JSON.stringify({ v: settings.permissionPreset === "
|
|
104227
|
+
return JSON.stringify({ v: settings.permissionPreset === "safe" ? 1 : 2, cwd: settings.cwd, permission: settings.permissionPreset, trusted: !!settings.projectTrusted, noSettings: !!settings.noSettingFiles });
|
|
103819
104228
|
}
|
|
103820
104229
|
async detect() {
|
|
103821
104230
|
const path2 = whichTool("claude");
|
|
@@ -104163,7 +104572,7 @@ var CodexClient = class {
|
|
|
104163
104572
|
};
|
|
104164
104573
|
|
|
104165
104574
|
// packages/agent-host/src/adapters/codex/rollout.ts
|
|
104166
|
-
import { createReadStream as
|
|
104575
|
+
import { createReadStream as createReadStream3 } from "node:fs";
|
|
104167
104576
|
import { readdir as readdir5 } from "node:fs/promises";
|
|
104168
104577
|
import { createInterface as createInterface2 } from "node:readline";
|
|
104169
104578
|
import { homedir as homedir6 } from "node:os";
|
|
@@ -104199,7 +104608,7 @@ async function lastCompactSummary(threadId, sessionsDir) {
|
|
|
104199
104608
|
if (!path2) return null;
|
|
104200
104609
|
try {
|
|
104201
104610
|
let last = null;
|
|
104202
|
-
const rl2 = createInterface2({ input:
|
|
104611
|
+
const rl2 = createInterface2({ input: createReadStream3(path2, "utf8"), crlfDelay: Infinity });
|
|
104203
104612
|
for await (const line of rl2) {
|
|
104204
104613
|
if (!line.includes('"compacted"')) continue;
|
|
104205
104614
|
try {
|
|
@@ -106321,10 +106730,10 @@ var CodexSession = class {
|
|
|
106321
106730
|
return this.opts.outputSchema ? { outputSchema: this.opts.outputSchema } : {};
|
|
106322
106731
|
}
|
|
106323
106732
|
respondApproval(requestId, decision, _scope, matcher) {
|
|
106324
|
-
const
|
|
106325
|
-
if (
|
|
106733
|
+
const open6 = this.approvals.get(requestId);
|
|
106734
|
+
if (open6 === void 0) return false;
|
|
106326
106735
|
this.approvals.delete(requestId);
|
|
106327
|
-
const serverId =
|
|
106736
|
+
const serverId = open6.serverId;
|
|
106328
106737
|
if (this.elicitations.delete(requestId)) {
|
|
106329
106738
|
this.client.respond(serverId, {
|
|
106330
106739
|
action: decision === "deny" ? "decline" : "accept",
|
|
@@ -106737,6 +107146,33 @@ function registeredTool(adapters2, preferred) {
|
|
|
106737
107146
|
return adapters2.keys().next().value ?? null;
|
|
106738
107147
|
}
|
|
106739
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
|
+
|
|
106740
107176
|
// packages/agent-host/src/dev-services/store.ts
|
|
106741
107177
|
import Database from "better-sqlite3";
|
|
106742
107178
|
import { existsSync as existsSync18, readFileSync as readFileSync12 } from "node:fs";
|
|
@@ -108055,6 +108491,70 @@ var Store = class _Store {
|
|
|
108055
108491
|
WHERE asked_by_session_id IS NULL AND parent_session_id IS NOT NULL AND worktree_path IS NOT NULL
|
|
108056
108492
|
`);
|
|
108057
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
|
+
}
|
|
108058
108558
|
}
|
|
108059
108559
|
];
|
|
108060
108560
|
}
|
|
@@ -108248,11 +108748,16 @@ var Store = class _Store {
|
|
|
108248
108748
|
this.checkpoint();
|
|
108249
108749
|
this.db.close();
|
|
108250
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
|
+
*/
|
|
108251
108756
|
addProject(p) {
|
|
108252
108757
|
this.db.prepare(
|
|
108253
|
-
`INSERT INTO projects (id, path, name, created_at) VALUES (?, ?, ?, ?)
|
|
108758
|
+
`INSERT INTO projects (id, path, name, created_at, trusted) VALUES (?, ?, ?, ?, ?)
|
|
108254
108759
|
ON CONFLICT(path) DO UPDATE SET name = excluded.name`
|
|
108255
|
-
).run(p.id, p.path, p.name, Date.now());
|
|
108760
|
+
).run(p.id, p.path, p.name, Date.now(), p.trusted ? 1 : 0);
|
|
108256
108761
|
}
|
|
108257
108762
|
/**
|
|
108258
108763
|
* `commands` is deliberately not in here (issue #44) — `projectCommands` answers that one.
|
|
@@ -108824,6 +109329,8 @@ var Store = class _Store {
|
|
|
108824
109329
|
this.db.prepare(`DELETE FROM commit_sessions WHERE session_id = ?`).run(sessionId);
|
|
108825
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);
|
|
108826
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);
|
|
108827
109334
|
this.db.prepare(`DELETE FROM sessions WHERE id = ? AND deleted_at IS NOT NULL`).run(sessionId);
|
|
108828
109335
|
return true;
|
|
108829
109336
|
});
|
|
@@ -109411,6 +109918,39 @@ var Store = class _Store {
|
|
|
109411
109918
|
).all();
|
|
109412
109919
|
return rows.filter((r) => ProjectConsentKind.safeParse(r.kind).success);
|
|
109413
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
|
+
}
|
|
109414
109954
|
// ── Linked machines (#82, v46) ─────────────────────────────────────────────────────────────
|
|
109415
109955
|
listLinkedMachines() {
|
|
109416
109956
|
const rows = this.db.prepare(
|
|
@@ -109580,9 +110120,9 @@ async function gitStatusFiles(cwd, trust = {}) {
|
|
|
109580
110120
|
if (entry.startsWith("1 ") || entry.startsWith("2 ")) {
|
|
109581
110121
|
const parts = entry.split(" ");
|
|
109582
110122
|
const xy = parts[1] ?? "..";
|
|
109583
|
-
const
|
|
109584
|
-
const path2 = parts.slice(
|
|
109585
|
-
if (
|
|
110123
|
+
const rename6 = entry.startsWith("2 ");
|
|
110124
|
+
const path2 = parts.slice(rename6 ? 9 : 8).join(" ");
|
|
110125
|
+
if (rename6) i++;
|
|
109586
110126
|
if (!path2) continue;
|
|
109587
110127
|
const [x, y] = [xy[0] ?? ".", xy[1] ?? "."];
|
|
109588
110128
|
if (x !== ".") out.push({ path: path2, staged: true, status: mapStatus(x) });
|
|
@@ -110100,7 +110640,7 @@ async function sweepAttachments(maxBytes = ATTACHMENTS_MAX_BYTES) {
|
|
|
110100
110640
|
|
|
110101
110641
|
// packages/agent-host/src/dev-services/message-image.ts
|
|
110102
110642
|
import { constants as constants7 } from "node:fs";
|
|
110103
|
-
import { open as
|
|
110643
|
+
import { open as open5, realpath as realpath7 } from "node:fs/promises";
|
|
110104
110644
|
import { homedir as homedir11 } from "node:os";
|
|
110105
110645
|
import { posix as posix3, sep as sep10, win32 as win326 } from "node:path";
|
|
110106
110646
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
@@ -110165,7 +110705,7 @@ async function readMessageImage(abs, max = IMAGE_PREVIEW_MAX_BYTES) {
|
|
|
110165
110705
|
}
|
|
110166
110706
|
let handle;
|
|
110167
110707
|
try {
|
|
110168
|
-
handle = await
|
|
110708
|
+
handle = await open5(file2, OPEN_FLAGS);
|
|
110169
110709
|
} catch (e) {
|
|
110170
110710
|
const code2 = e.code;
|
|
110171
110711
|
if (code2 === "EACCES" || code2 === "EPERM") return refuse5("unreadable", `Centralu may not read ${file2}`, file2);
|
|
@@ -110266,12 +110806,72 @@ function attachCommitSessions(commits, records, nameOf) {
|
|
|
110266
110806
|
|
|
110267
110807
|
// packages/agent-host/src/sessions/manager.ts
|
|
110268
110808
|
function withTimeout(p, ms3, label = "A call") {
|
|
110809
|
+
let timer;
|
|
110269
110810
|
return Promise.race([
|
|
110270
110811
|
p,
|
|
110271
|
-
new Promise(
|
|
110272
|
-
|
|
110273
|
-
)
|
|
110274
|
-
]);
|
|
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
|
+
}
|
|
110275
110875
|
}
|
|
110276
110876
|
var HISTORY_LIMIT = 200;
|
|
110277
110877
|
var SYNC_LIMIT = 600;
|
|
@@ -110308,10 +110908,26 @@ function isCompactionMarker(row) {
|
|
|
110308
110908
|
return row.kind === "marker" && p?.type === "compaction" && !p.failed;
|
|
110309
110909
|
}
|
|
110310
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
|
+
}
|
|
110311
110926
|
var EXTERNAL_LIST_LIMIT = 200;
|
|
110312
110927
|
var HOST_READ_MAX_LINES = 200;
|
|
110313
110928
|
var HOST_RECALL_MAX_HITS = 50;
|
|
110314
110929
|
var HUB_SENDER = "_hub-orchestrator";
|
|
110930
|
+
var HOST_SENDER = "_centralu";
|
|
110315
110931
|
var builderKey = (ref) => `${ref.projectId ?? "_user"}/${ref.appId}`;
|
|
110316
110932
|
var APP_BUILDERS_KEY = "apps.builders";
|
|
110317
110933
|
var SessionManager = class {
|
|
@@ -110344,8 +110960,29 @@ var SessionManager = class {
|
|
|
110344
110960
|
store2.upsertSession(fixed);
|
|
110345
110961
|
}
|
|
110346
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
|
+
}
|
|
110347
110983
|
void this.sweepOrphanHandoffNotes().catch(() => {
|
|
110348
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}`));
|
|
110349
110986
|
this.nameUnnamedWorktrees();
|
|
110350
110987
|
for (const pid of new Set(
|
|
110351
110988
|
[...this.meta.values()].filter((s) => s.worktree && s.projectId).map((s) => s.projectId)
|
|
@@ -110416,8 +111053,30 @@ var SessionManager = class {
|
|
|
110416
111053
|
cacheLedger = new CacheLedger();
|
|
110417
111054
|
/** Why a session's process is being restarted, for the cache's rebuild cause; taken when the new process comes up */
|
|
110418
111055
|
restartReasons = /* @__PURE__ */ new Map();
|
|
110419
|
-
/**
|
|
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
|
+
*/
|
|
110420
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
|
+
}
|
|
110421
111080
|
/**
|
|
110422
111081
|
* Session currently being resumed -> its promise.
|
|
110423
111082
|
*
|
|
@@ -110447,9 +111106,31 @@ var SessionManager = class {
|
|
|
110447
111106
|
definitionsRestartAfterTurn = /* @__PURE__ */ new Set();
|
|
110448
111107
|
/** A kept process was started with adapter settings from an older host build (#589). */
|
|
110449
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();
|
|
110450
111127
|
updates = null;
|
|
110451
111128
|
/** Sessions allowed to update once per host; an "always" answer adds this session only. */
|
|
110452
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();
|
|
110453
111134
|
/**
|
|
110454
111135
|
* Where external apps are attached to a session (M4 A-5). It is null on a host with no runtime
|
|
110455
111136
|
* (most tests), and in that case the session accepts no apps — it is optional, like any other
|
|
@@ -110499,6 +111180,21 @@ var SessionManager = class {
|
|
|
110499
111180
|
* `readGrants`: a restart asks again. The project-wide always is a stored consent instead.
|
|
110500
111181
|
*/
|
|
110501
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;
|
|
110502
111198
|
/**
|
|
110503
111199
|
* What a session may read beyond its folder because another project handed it back (#371 part B) — session id ->
|
|
110504
111200
|
* resolved paths (files, or output folders). Live only: the grant lasts while this host serves the session, and a
|
|
@@ -110551,13 +111247,18 @@ var SessionManager = class {
|
|
|
110551
111247
|
{ code: "internal" }
|
|
110552
111248
|
);
|
|
110553
111249
|
}
|
|
110554
|
-
|
|
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 = {}) {
|
|
110555
111256
|
if (!existsSync19(path2) || !statSync12(path2).isDirectory()) {
|
|
110556
111257
|
throw Object.assign(new Error(`Directory not found: ${path2}`), { code: "internal" });
|
|
110557
111258
|
}
|
|
110558
111259
|
const existing = this.store.findProjectByPath(path2);
|
|
110559
111260
|
const id2 = existing?.id ?? randomUUID10();
|
|
110560
|
-
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 } : {} });
|
|
110561
111262
|
return this.projectInfo(id2, path2);
|
|
110562
111263
|
}
|
|
110563
111264
|
/**
|
|
@@ -110602,7 +111303,7 @@ var SessionManager = class {
|
|
|
110602
111303
|
/**
|
|
110603
111304
|
* Makes a new project (#515): a folder named `name` in `parent` (this machine's `~/Centralu` when absent), empty, or a
|
|
110604
111305
|
* clone of `source` made with this machine's git and saved sign-ins, then registers it. Never reuses anything
|
|
110605
|
-
* 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
|
|
110606
111307
|
* and no project is registered.
|
|
110607
111308
|
*/
|
|
110608
111309
|
async createProject(name, parent, source) {
|
|
@@ -110619,7 +111320,7 @@ var SessionManager = class {
|
|
|
110619
111320
|
}
|
|
110620
111321
|
}
|
|
110621
111322
|
console.error(`[projects] made ${target.path}`);
|
|
110622
|
-
const info = await this.addProject(target.path);
|
|
111323
|
+
const info = await this.addProject(target.path, { trusted: !source });
|
|
110623
111324
|
this.emit({ type: "projects_changed" });
|
|
110624
111325
|
return info;
|
|
110625
111326
|
}
|
|
@@ -110713,13 +111414,29 @@ var SessionManager = class {
|
|
|
110713
111414
|
}
|
|
110714
111415
|
/**
|
|
110715
111416
|
* Project trust (M4 A-2, plan decision 3). The app runtime reads it fresh from the store every
|
|
110716
|
-
* time, so
|
|
110717
|
-
*
|
|
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.
|
|
110718
111424
|
*/
|
|
110719
111425
|
setProjectTrusted(projectId, trusted) {
|
|
110720
111426
|
if (!this.store.setProjectTrusted(projectId, trusted)) {
|
|
110721
111427
|
throw Object.assign(new Error(`Project not found: ${projectId}`), { code: "internal" });
|
|
110722
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);
|
|
110723
111440
|
}
|
|
110724
111441
|
/**
|
|
110725
111442
|
* Is this project trusted (decision 3, #92) — passed to the adapter when launching a session. False
|
|
@@ -111169,6 +111886,7 @@ var SessionManager = class {
|
|
|
111169
111886
|
let handle;
|
|
111170
111887
|
if (params.builderOf) this.setBuilder(params.builderOf, id2);
|
|
111171
111888
|
const apps = this.appsFor(info);
|
|
111889
|
+
const profile = toolProfileFor(this.profileFacts(info, !!params.builderOf));
|
|
111172
111890
|
const from = this.handleSink();
|
|
111173
111891
|
this.unborn.set(id2, []);
|
|
111174
111892
|
try {
|
|
@@ -111192,14 +111910,14 @@ var SessionManager = class {
|
|
|
111192
111910
|
...this.handoffReadDirs(params.projectId, !!params.handoff?.fromSessionId),
|
|
111193
111911
|
// Paths another project handed back to this session (#371), asked on every read
|
|
111194
111912
|
mayRead: (path2) => this.mayRead(id2, path2, cwd),
|
|
111195
|
-
// An orchestrator gets all of them
|
|
111196
|
-
//
|
|
111197
|
-
orchestratorTools:
|
|
111198
|
-
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,
|
|
111199
111917
|
systemPromptAppend: info.kind === "orchestrator" ? orchestratorRole() + this.skillsPrompt() : info.roleAppend ?? void 0,
|
|
111200
111918
|
// The path back to the host — used by the orchestrator tools' bridge and an external app's bridge
|
|
111201
111919
|
// (M4 A-5)
|
|
111202
|
-
orchestratorBridge:
|
|
111920
|
+
orchestratorBridge: profile || apps ? this.endpoint?.(id2) ?? void 0 : void 0,
|
|
111203
111921
|
// An MCP server the person approved is loaded here as a user-folder app (M4 A-7, decision 4)
|
|
111204
111922
|
apps,
|
|
111205
111923
|
// The answer schema an app supplied when it asked for this (M4 D-1) — Claude gets it on this
|
|
@@ -111235,6 +111953,7 @@ var SessionManager = class {
|
|
|
111235
111953
|
}
|
|
111236
111954
|
}
|
|
111237
111955
|
this.handles.set(id2, handle);
|
|
111956
|
+
this.rememberBackgroundTasks(info);
|
|
111238
111957
|
this.cacheLedger.note(id2, params.resumeExternalId ? "new_process" : "cold_start");
|
|
111239
111958
|
this.running.set(id2, {
|
|
111240
111959
|
model: info.model,
|
|
@@ -111439,26 +112158,96 @@ var SessionManager = class {
|
|
|
111439
112158
|
* The keeper as a `ProcessSource` for one session, when this host runs under one (#280 step 2). `version` is the CLI
|
|
111440
112159
|
* version a process spawned now is started from, written on its keeper tag (#297).
|
|
111441
112160
|
*/
|
|
111442
|
-
processSourceFor(sessionId, tool, adopt, version2) {
|
|
112161
|
+
processSourceFor(sessionId, tool, adopt, version2, kept) {
|
|
111443
112162
|
const host = this.keeperOpts.processes;
|
|
111444
112163
|
if (!host) return {};
|
|
111445
112164
|
return {
|
|
111446
112165
|
processSource: {
|
|
111447
112166
|
spawn: (spec) => host.spawn(sessionId, tool, spec, version2 ?? void 0),
|
|
111448
|
-
...adopt ? { adopt: { process: adopt, openCalls: this.openToolCalls(sessionId) } } : {}
|
|
112167
|
+
...adopt ? { adopt: { process: adopt, openCalls: this.openToolCalls(sessionId), backgroundTasks: kept ?? [] } } : {}
|
|
111449
112168
|
}
|
|
111450
112169
|
};
|
|
111451
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
|
+
}
|
|
111452
112241
|
/** Tool calls the store holds for this session with no result after them (the newest 200 rows) */
|
|
111453
112242
|
openToolCalls(sessionId) {
|
|
111454
|
-
const
|
|
112243
|
+
const open6 = /* @__PURE__ */ new Map();
|
|
111455
112244
|
for (const r of this.store.loadMessages(sessionId, 200)) {
|
|
111456
112245
|
const p = r.payload;
|
|
111457
112246
|
if (typeof p?.callId !== "string") continue;
|
|
111458
|
-
if (r.kind === "tool_call" && typeof p.summary?.tool === "string")
|
|
111459
|
-
else if (r.kind === "tool_result")
|
|
112247
|
+
if (r.kind === "tool_call" && typeof p.summary?.tool === "string") open6.set(p.callId, p.summary.tool);
|
|
112248
|
+
else if (r.kind === "tool_result") open6.delete(p.callId);
|
|
111460
112249
|
}
|
|
111461
|
-
return [...
|
|
112250
|
+
return [...open6].map(([callId, tool]) => ({ callId, tool }));
|
|
111462
112251
|
}
|
|
111463
112252
|
/**
|
|
111464
112253
|
* Re-attaches the tool processes a previous host left running in the keeper (#280 step 2), so
|
|
@@ -111491,7 +112280,7 @@ var SessionManager = class {
|
|
|
111491
112280
|
);
|
|
111492
112281
|
}
|
|
111493
112282
|
async doResumeSession(sessionId, adopt) {
|
|
111494
|
-
if (this.
|
|
112283
|
+
if (this.worktreeCleanupBlocks(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
111495
112284
|
const m = this.meta.get(sessionId);
|
|
111496
112285
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
111497
112286
|
if (this.handles.has(sessionId)) return { session: m, resumed: true };
|
|
@@ -111533,6 +112322,7 @@ var SessionManager = class {
|
|
|
111533
112322
|
}
|
|
111534
112323
|
const apps = this.appsFor(m);
|
|
111535
112324
|
const builds = this.builderRefOf(m) !== null;
|
|
112325
|
+
const profile = toolProfileFor(this.profileFacts(m, builds));
|
|
111536
112326
|
const from = this.handleSink();
|
|
111537
112327
|
const launched = {
|
|
111538
112328
|
model: m.model,
|
|
@@ -111542,12 +112332,13 @@ var SessionManager = class {
|
|
|
111542
112332
|
permissionPreset: m.permissionPreset
|
|
111543
112333
|
};
|
|
111544
112334
|
if (!adopt) m.agentVersion = this.versionHint?.(tool) ?? null;
|
|
112335
|
+
const kept = adopt ? this.keptBackgroundTasks(sessionId) : null;
|
|
111545
112336
|
try {
|
|
111546
112337
|
const creating = adapter.createSession(
|
|
111547
112338
|
{
|
|
111548
112339
|
sessionId,
|
|
111549
112340
|
cwd,
|
|
111550
|
-
...this.processSourceFor(sessionId, tool, adopt, m.agentVersion),
|
|
112341
|
+
...this.processSourceFor(sessionId, tool, adopt, m.agentVersion, kept),
|
|
111551
112342
|
model: launched.model ?? void 0,
|
|
111552
112343
|
effort: launched.effort ?? void 0,
|
|
111553
112344
|
verbosity: launched.verbosity ?? void 0,
|
|
@@ -111579,11 +112370,8 @@ var SessionManager = class {
|
|
|
111579
112370
|
* has just been attached passes this check the next time it wakes and receives the
|
|
111580
112371
|
* child-only tools and role.
|
|
111581
112372
|
*/
|
|
111582
|
-
orchestratorTools:
|
|
111583
|
-
|
|
111584
|
-
this.readsOwnProject(m) ? this.readerToolsFor(sessionId) : void 0
|
|
111585
|
-
),
|
|
111586
|
-
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,
|
|
111587
112375
|
/*
|
|
111588
112376
|
* This is **where memory is handed over.** Switching tools breaks externalId, which routes
|
|
111589
112377
|
* things down this path (a new process), and that is when the past conversation rides along
|
|
@@ -111599,7 +112387,7 @@ var SessionManager = class {
|
|
|
111599
112387
|
// for a builder session (C-2) even if the app is broken
|
|
111600
112388
|
m.roleAppend ?? void 0
|
|
111601
112389
|
),
|
|
111602
|
-
orchestratorBridge:
|
|
112390
|
+
orchestratorBridge: profile || apps ? this.endpoint?.(sessionId) ?? void 0 : void 0,
|
|
111603
112391
|
// An approved MCP server is a user-folder app — an orchestrator receives those apps right here (M4
|
|
111604
112392
|
// A-7)
|
|
111605
112393
|
apps
|
|
@@ -111625,6 +112413,8 @@ var SessionManager = class {
|
|
|
111625
112413
|
if (!adopt) {
|
|
111626
112414
|
this.rememberToolPrefix(sessionId);
|
|
111627
112415
|
this.rememberLaunchSettings(m, cwd);
|
|
112416
|
+
this.backgroundUnknown.delete(sessionId);
|
|
112417
|
+
this.rememberBackgroundTasks(m);
|
|
111628
112418
|
}
|
|
111629
112419
|
this.emit({ type: "session_live", sessionId, live: true });
|
|
111630
112420
|
this.cacheLedger.note(sessionId, this.restartReasons.get(sessionId) ?? "new_process");
|
|
@@ -111635,6 +112425,8 @@ var SessionManager = class {
|
|
|
111635
112425
|
if (adopt) {
|
|
111636
112426
|
this.store.upsertSession(m);
|
|
111637
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 });
|
|
111638
112430
|
} else {
|
|
111639
112431
|
m.state = "idle";
|
|
111640
112432
|
m.waitingSince = null;
|
|
@@ -111680,7 +112472,7 @@ var SessionManager = class {
|
|
|
111680
112472
|
* exactly as it was, and only what happens from here on piles up in the copy.
|
|
111681
112473
|
*/
|
|
111682
112474
|
async forkConversation(sessionId) {
|
|
111683
|
-
if (this.
|
|
112475
|
+
if (this.worktreeCleanupBlocks(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
111684
112476
|
const m = this.meta.get(sessionId);
|
|
111685
112477
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
111686
112478
|
const source = m.externalId ?? m.importedFrom;
|
|
@@ -111713,6 +112505,7 @@ var SessionManager = class {
|
|
|
111713
112505
|
this.leaving.delete(sessionId);
|
|
111714
112506
|
}
|
|
111715
112507
|
}
|
|
112508
|
+
trashing = /* @__PURE__ */ new Map();
|
|
111716
112509
|
/**
|
|
111717
112510
|
* Moves a session to the trash (#204) — what deleting a session means now. Nothing is destroyed: the process
|
|
111718
112511
|
* stops, and the session leaves every list (sidebar, inbox, palette, grid, search, the agents' session tools and
|
|
@@ -111732,6 +112525,20 @@ var SessionManager = class {
|
|
|
111732
112525
|
* tool, and **+ → Past conversations**, still have it.
|
|
111733
112526
|
*/
|
|
111734
112527
|
async trashSession(sessionId, removeWorktree = false, removeExternal = false, recovery) {
|
|
112528
|
+
const pending = this.trashing.get(sessionId);
|
|
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
|
+
}
|
|
112533
|
+
const deleting = this.trashSessionOnce(sessionId, removeWorktree, removeExternal, recovery);
|
|
112534
|
+
this.trashing.set(sessionId, deleting);
|
|
112535
|
+
try {
|
|
112536
|
+
await deleting;
|
|
112537
|
+
} finally {
|
|
112538
|
+
this.trashing.delete(sessionId);
|
|
112539
|
+
}
|
|
112540
|
+
}
|
|
112541
|
+
async trashSessionOnce(sessionId, removeWorktree, removeExternal, recovery) {
|
|
111735
112542
|
if (this.worktreeCleanup.has(sessionId) && !recovery) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
111736
112543
|
const m = this.meta.get(sessionId);
|
|
111737
112544
|
if (!m) {
|
|
@@ -111756,6 +112563,13 @@ var SessionManager = class {
|
|
|
111756
112563
|
}
|
|
111757
112564
|
this.readGrants.delete(sessionId);
|
|
111758
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);
|
|
111759
112573
|
for (const ask of [...this.hostAsks.values()]) {
|
|
111760
112574
|
if (ask.sessionId !== sessionId) continue;
|
|
111761
112575
|
this.hostAsks.delete(ask.requestId);
|
|
@@ -111767,14 +112581,18 @@ var SessionManager = class {
|
|
|
111767
112581
|
});
|
|
111768
112582
|
this.handles.delete(sessionId);
|
|
111769
112583
|
}
|
|
112584
|
+
this.forgetBackground(sessionId);
|
|
111770
112585
|
this.closeStream(sessionId);
|
|
111771
112586
|
let project2;
|
|
111772
112587
|
if (m.projectId) project2 = this.store.listProjects().find((p) => p.id === m.projectId);
|
|
111773
112588
|
this.running.delete(sessionId);
|
|
111774
112589
|
this.restartAfterTurn.delete(sessionId);
|
|
112590
|
+
this.trustRestartNotice.delete(sessionId);
|
|
111775
112591
|
this.prCheckedAt.delete(sessionId);
|
|
111776
112592
|
this.appsRestartAfterTurn.delete(sessionId);
|
|
111777
|
-
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);
|
|
111778
112596
|
this.forgetCommits(sessionId);
|
|
111779
112597
|
this.meta.delete(sessionId);
|
|
111780
112598
|
await this.store.trashSession(sessionId, {
|
|
@@ -111788,6 +112606,8 @@ var SessionManager = class {
|
|
|
111788
112606
|
this.appsHub?.sessionGone(sessionId);
|
|
111789
112607
|
this.emit({ type: "session_deleted", sessionId });
|
|
111790
112608
|
}
|
|
112609
|
+
/** The startup repair of recovery folders an interrupted purge left aside (#647) */
|
|
112610
|
+
recoveryRepair = Promise.resolve();
|
|
111791
112611
|
/** Only one trash operation per session at a time — a purge racing a restore would restore half of a deleted session */
|
|
111792
112612
|
trashBusy = /* @__PURE__ */ new Set();
|
|
111793
112613
|
async trashOp(sessionId, run) {
|
|
@@ -111852,6 +112672,7 @@ var SessionManager = class {
|
|
|
111852
112672
|
*/
|
|
111853
112673
|
async restoreSession(sessionId) {
|
|
111854
112674
|
return this.trashOp(sessionId, async () => {
|
|
112675
|
+
await this.recoveryRepair;
|
|
111855
112676
|
const t = this.store.trashedSession(sessionId);
|
|
111856
112677
|
if (!t) throw Object.assign(new Error(`Not in the trash: ${sessionId}`), { code: "session_not_found" });
|
|
111857
112678
|
const { record: record2 } = t;
|
|
@@ -111908,10 +112729,12 @@ var SessionManager = class {
|
|
|
111908
112729
|
* believes 550MB is gone). It is skipped when a live session holds the same conversation (it was pulled back
|
|
111909
112730
|
* from Past conversations meanwhile): deleting it would take that session's history. Then the worktree, when
|
|
111910
112731
|
* chosen (a failure does not stop the purge — its folder is plain files the person can still remove). Then our
|
|
111911
|
-
* 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.
|
|
111912
112734
|
*/
|
|
111913
112735
|
async purgeSession(sessionId) {
|
|
111914
112736
|
return this.trashOp(sessionId, async () => {
|
|
112737
|
+
await this.recoveryRepair;
|
|
111915
112738
|
const t = this.store.trashedSession(sessionId);
|
|
111916
112739
|
if (!t) throw Object.assign(new Error(`Not in the trash: ${sessionId}`), { code: "session_not_found" });
|
|
111917
112740
|
const { session: s, record: record2 } = t;
|
|
@@ -111934,8 +112757,16 @@ var SessionManager = class {
|
|
|
111934
112757
|
await gitWorktreeRemove(record2.projectPath, s.worktree.path, true).catch(() => {
|
|
111935
112758
|
});
|
|
111936
112759
|
}
|
|
111937
|
-
await
|
|
111938
|
-
|
|
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
|
+
});
|
|
111939
112770
|
await clearAttachments(sessionId).catch(() => {
|
|
111940
112771
|
});
|
|
111941
112772
|
if (record2.projectId) await this.sweepOrphanHandoffNotes(record2.projectId).catch(() => {
|
|
@@ -112006,8 +112837,8 @@ var SessionManager = class {
|
|
|
112006
112837
|
const m = this.meta.get(sessionId);
|
|
112007
112838
|
return live && m && live.model !== m.model ? "model_change" : void 0;
|
|
112008
112839
|
}
|
|
112009
|
-
async updateSettings(sessionId, s) {
|
|
112010
|
-
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" });
|
|
112011
112842
|
const m = this.meta.get(sessionId);
|
|
112012
112843
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
112013
112844
|
if (s.model !== void 0 && s.model !== m.model) this.cacheLedger.note(sessionId, "model_change");
|
|
@@ -112020,6 +112851,16 @@ var SessionManager = class {
|
|
|
112020
112851
|
if ((s.model !== void 0 || s.effort !== void 0) && m.projectId) {
|
|
112021
112852
|
this.store.setProjectToolDefaults(m.projectId, m.tool, { model: m.model, effort: m.effort });
|
|
112022
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
|
+
});
|
|
112023
112864
|
await this.resuming.get(sessionId)?.catch(() => {
|
|
112024
112865
|
});
|
|
112025
112866
|
const handle = this.handles.get(sessionId);
|
|
@@ -112102,6 +112943,7 @@ var SessionManager = class {
|
|
|
112102
112943
|
}
|
|
112103
112944
|
const wasBusy = inTurn(m.state);
|
|
112104
112945
|
this.applyStateHint(e, m);
|
|
112946
|
+
if (this.backgroundUnknown.has(m.id)) this.backgroundUnknown.set(m.id, Date.now());
|
|
112105
112947
|
if (wasBusy && !inTurn(m.state)) {
|
|
112106
112948
|
endedTurn = true;
|
|
112107
112949
|
const ref = this.builderRefOf(m);
|
|
@@ -112119,6 +112961,7 @@ var SessionManager = class {
|
|
|
112119
112961
|
});
|
|
112120
112962
|
this.handles.delete(e.sessionId);
|
|
112121
112963
|
this.running.delete(e.sessionId);
|
|
112964
|
+
this.emit({ type: "session_live", sessionId: e.sessionId, live: false, reason: "crashed" });
|
|
112122
112965
|
}
|
|
112123
112966
|
}
|
|
112124
112967
|
}
|
|
@@ -112128,13 +112971,37 @@ var SessionManager = class {
|
|
|
112128
112971
|
this.emit(seq != null ? { ...e, seq } : e);
|
|
112129
112972
|
if (endedTurn && e.sessionId && !run?.tracksInputs) void this.reportBackIfAwaited(e.sessionId, e, run?.completedAnswer);
|
|
112130
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);
|
|
112131
112975
|
if (endedTurn && e.sessionId) this.forgetCommits(e.sessionId);
|
|
112132
112976
|
if (endedTurn && e.sessionId) this.applyDeferredRestart(e.sessionId);
|
|
112977
|
+
if (e.type === "background_tasks" && e.sessionId) this.applyDeferredRestart(e.sessionId);
|
|
112133
112978
|
}
|
|
112134
|
-
/** 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
|
+
*/
|
|
112135
112994
|
applyDeferredRestart(sessionId) {
|
|
112136
112995
|
const session2 = this.meta.get(sessionId);
|
|
112137
|
-
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);
|
|
112138
113005
|
const apps = this.appsRestartAfterTurn.delete(sessionId);
|
|
112139
113006
|
const definitions = this.definitionsRestartAfterTurn.delete(sessionId);
|
|
112140
113007
|
const launchSettings = this.launchSettingsRestartAfterTurn.delete(sessionId);
|
|
@@ -112148,6 +113015,28 @@ var SessionManager = class {
|
|
|
112148
113015
|
void restarting.catch(() => {
|
|
112149
113016
|
});
|
|
112150
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
|
+
}
|
|
112151
113040
|
/**
|
|
112152
113041
|
* Notifies the orchestrator when the work it assigned finishes — **the other half of "one window".**
|
|
112153
113042
|
*
|
|
@@ -112162,8 +113051,8 @@ var SessionManager = class {
|
|
|
112162
113051
|
*/
|
|
112163
113052
|
async reportBackIfAwaited(sessionId, ending, answer) {
|
|
112164
113053
|
const orchestratorId = this.awaitingReport.get(sessionId);
|
|
112165
|
-
if (!orchestratorId) return;
|
|
112166
|
-
this.
|
|
113054
|
+
if (!orchestratorId || this.handingOver) return;
|
|
113055
|
+
this.clearReport(sessionId);
|
|
112167
113056
|
const target = this.meta.get(sessionId);
|
|
112168
113057
|
if (!target || !this.meta.has(orchestratorId)) return;
|
|
112169
113058
|
const project2 = target.projectId ? this.store.listProjects().find((p) => p.id === target.projectId)?.name ?? "(project no longer exists)" : "(none)";
|
|
@@ -112268,6 +113157,8 @@ If you need more, you can read that session's recent conversation with read_sess
|
|
|
112268
113157
|
break;
|
|
112269
113158
|
case "background_tasks":
|
|
112270
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);
|
|
112271
113162
|
break;
|
|
112272
113163
|
case "agent_version":
|
|
112273
113164
|
m.agentVersion = e.version;
|
|
@@ -112506,7 +113397,7 @@ If you need more, you can read that session's recent conversation with read_sess
|
|
|
112506
113397
|
async deliver(sessionId, text2, attachments, from, relayed, fromApp, fromAppVia = "inline", sendInput) {
|
|
112507
113398
|
const restarting = this.deferredRestarts.get(sessionId);
|
|
112508
113399
|
if (restarting) await restarting;
|
|
112509
|
-
if (this.
|
|
113400
|
+
if (this.worktreeCleanupBlocks(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
112510
113401
|
const m = this.meta.get(sessionId);
|
|
112511
113402
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
112512
113403
|
if (!this.handles.has(sessionId)) {
|
|
@@ -112517,7 +113408,7 @@ If you need more, you can read that session's recent conversation with read_sess
|
|
|
112517
113408
|
});
|
|
112518
113409
|
}
|
|
112519
113410
|
}
|
|
112520
|
-
if (this.
|
|
113411
|
+
if (this.worktreeCleanupBlocks(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
112521
113412
|
const h = this.requireHandle(sessionId);
|
|
112522
113413
|
this.closeStream(sessionId);
|
|
112523
113414
|
const seq = this.store.nextSeq(sessionId);
|
|
@@ -112535,7 +113426,7 @@ If you need more, you can read that session's recent conversation with read_sess
|
|
|
112535
113426
|
]);
|
|
112536
113427
|
m.lastSeq = seq;
|
|
112537
113428
|
m.lastReadSeq = seq;
|
|
112538
|
-
if (m.autoNamed && m.name === "New session" && !fromApp) {
|
|
113429
|
+
if (m.autoNamed && m.name === "New session" && !fromApp && !from) {
|
|
112539
113430
|
m.name = truncate(text2);
|
|
112540
113431
|
this.emit({ type: "session_title", sessionId, title: m.name, auto: true });
|
|
112541
113432
|
}
|
|
@@ -112572,7 +113463,7 @@ ${attachments.map((a) => `@${a.path}`).join("\n")}` : text2;
|
|
|
112572
113463
|
* untouched, and only the tool underneath it is swapped.
|
|
112573
113464
|
*/
|
|
112574
113465
|
async switchTool(sessionId, tool) {
|
|
112575
|
-
if (this.
|
|
113466
|
+
if (this.worktreeCleanupBlocks(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
112576
113467
|
const m = this.meta.get(sessionId);
|
|
112577
113468
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
112578
113469
|
if (m.tool === tool) return { ...m, live: this.handles.has(sessionId) };
|
|
@@ -112609,8 +113500,9 @@ ${attachments.map((a) => `@${a.path}`).join("\n")}` : text2;
|
|
|
112609
113500
|
const ask = this.hostAsks.get(requestId);
|
|
112610
113501
|
if (ask && ask.sessionId === sessionId) {
|
|
112611
113502
|
this.hostAsks.delete(requestId);
|
|
112612
|
-
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);
|
|
112613
113504
|
const answer = !keepsAlways && decision === "always" ? "allow" : decision;
|
|
113505
|
+
this.restAfterCard(ask);
|
|
112614
113506
|
this.onEvent({ type: "approval_resolved", sessionId, requestId, decision: answer });
|
|
112615
113507
|
ask.resolve(ask.detail.kind === "agent_start" && answer === "always" && scope === "project" ? "always_project" : answer);
|
|
112616
113508
|
const after = this.meta.get(sessionId);
|
|
@@ -112786,6 +113678,37 @@ ${attachments.map((a) => `@${a.path}`).join("\n")}` : text2;
|
|
|
112786
113678
|
* opens would make that click feel slow.
|
|
112787
113679
|
*/
|
|
112788
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
|
+
}
|
|
112789
113712
|
async listModels(tool) {
|
|
112790
113713
|
const adapter = this.adapters.get(tool);
|
|
112791
113714
|
if (!adapter?.listModels) {
|
|
@@ -112794,13 +113717,40 @@ ${attachments.map((a) => `@${a.path}`).join("\n")}` : text2;
|
|
|
112794
113717
|
const hit = this.modelCache.get(tool);
|
|
112795
113718
|
if (hit && Date.now() - hit.at < 5 * 6e4) return { supported: true, models: hit.models };
|
|
112796
113719
|
try {
|
|
112797
|
-
const models = await withTimeout(
|
|
112798
|
-
this.modelCache.set(tool, { models, at: Date.now() });
|
|
113720
|
+
const models = await withTimeout(this.fetchModels(tool, adapter), 15e3);
|
|
112799
113721
|
return { supported: true, models };
|
|
112800
113722
|
} catch (err) {
|
|
112801
113723
|
return { supported: false, reason: err.message, models: hit?.models ?? [] };
|
|
112802
113724
|
}
|
|
112803
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
|
+
}
|
|
112804
113754
|
/** A project's working directory. Used by the terminal to decide its own key (cwd) */
|
|
112805
113755
|
cwdOfProject(projectId) {
|
|
112806
113756
|
return this.cwdOf(projectId);
|
|
@@ -113237,13 +114187,14 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113237
114187
|
* reports background work.
|
|
113238
114188
|
*/
|
|
113239
114189
|
sessionIdle(sessionId) {
|
|
113240
|
-
if (this.
|
|
114190
|
+
if (this.worktreeCleanupBlocks(sessionId)) return { idle: false, reason: "turn" };
|
|
113241
114191
|
const m = this.meta.get(sessionId);
|
|
113242
114192
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
113243
|
-
if (this.
|
|
114193
|
+
if (this.inputsHeld(sessionId)) return { idle: false, reason: "turn" };
|
|
113244
114194
|
return sessionIdle(m, {
|
|
113245
114195
|
running: this.handles.has(sessionId),
|
|
113246
|
-
|
|
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)
|
|
113247
114198
|
});
|
|
113248
114199
|
}
|
|
113249
114200
|
/**
|
|
@@ -113303,7 +114254,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113303
114254
|
* process.
|
|
113304
114255
|
*/
|
|
113305
114256
|
async restartSession(sessionId, cause2) {
|
|
113306
|
-
if (this.
|
|
114257
|
+
if (this.worktreeCleanupBlocks(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
113307
114258
|
if (cause2) {
|
|
113308
114259
|
this.restartReasons.set(sessionId, cause2);
|
|
113309
114260
|
if (cause2 === "launch_settings") console.error(`[sessions] restarting ${sessionId}: launch settings changed or previously unrecorded`);
|
|
@@ -113312,8 +114263,11 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113312
114263
|
this.appsRestartAfterTurn.delete(sessionId);
|
|
113313
114264
|
this.definitionsRestartAfterTurn.delete(sessionId);
|
|
113314
114265
|
this.launchSettingsRestartAfterTurn.delete(sessionId);
|
|
114266
|
+
this.noticeTrustRestart(sessionId);
|
|
114267
|
+
this.clearBackgroundHold(sessionId);
|
|
113315
114268
|
const h = this.handles.get(sessionId);
|
|
113316
114269
|
if (h) {
|
|
114270
|
+
this.noticeBackgroundStopped(sessionId, cause2);
|
|
113317
114271
|
await h.dispose().catch(() => {
|
|
113318
114272
|
});
|
|
113319
114273
|
this.stampExternalSynced(sessionId);
|
|
@@ -113321,7 +114275,17 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113321
114275
|
this.running.delete(sessionId);
|
|
113322
114276
|
this.closeStream(sessionId);
|
|
113323
114277
|
}
|
|
113324
|
-
|
|
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;
|
|
113325
114289
|
}
|
|
113326
114290
|
/** The persisted prefix a process was started with. It is separate from SessionInfo: a screen never needs it. */
|
|
113327
114291
|
toolPrefixKey(sessionId) {
|
|
@@ -113393,8 +114357,9 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113393
114357
|
* environment). The adapter owns the fingerprint; the host only gives it the same safe
|
|
113394
114358
|
* boundary behaviour as changed tool definitions.
|
|
113395
114359
|
*/
|
|
113396
|
-
async refreshLaunchSettings() {
|
|
113397
|
-
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;
|
|
113398
114363
|
const cwd = this.store.sessionCwd(session2.id);
|
|
113399
114364
|
if (!cwd) continue;
|
|
113400
114365
|
const next = this.launchSettingsFingerprint(session2, cwd);
|
|
@@ -113403,6 +114368,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113403
114368
|
this.store.setAppSetting(this.launchSettingsKey(session2.id), next);
|
|
113404
114369
|
continue;
|
|
113405
114370
|
}
|
|
114371
|
+
if (trustChange) this.trustRestartNotice.add(session2.id);
|
|
113406
114372
|
if (inTurn(session2.state)) {
|
|
113407
114373
|
this.restartAfterTurn.add(session2.id);
|
|
113408
114374
|
this.launchSettingsRestartAfterTurn.add(session2.id);
|
|
@@ -113427,7 +114393,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113427
114393
|
const off = () => !this.uiPreferences().sessionTools;
|
|
113428
114394
|
const offError = "Turned off in Settings";
|
|
113429
114395
|
return {
|
|
113430
|
-
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),
|
|
113431
114397
|
listSessions: async () => off() ? [] : view2.listSessions(),
|
|
113432
114398
|
readSession: async (...a) => off() ? { ok: false, error: offError } : view2.readSession(...a),
|
|
113433
114399
|
recall: async (...a) => off() ? { hits: [] } : view2.recall(...a),
|
|
@@ -113473,7 +114439,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113473
114439
|
);
|
|
113474
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}`;
|
|
113475
114441
|
return {
|
|
113476
|
-
updateCentralu: async (action) => this.updateCentraluAsked(orchestratorId, action),
|
|
114442
|
+
updateCentralu: async (action, signal) => this.updateCentraluAsked(orchestratorId, action, signal),
|
|
113477
114443
|
listSessions: async () => {
|
|
113478
114444
|
const byId = projects();
|
|
113479
114445
|
const mine = [...this.meta.values()].filter((s) => s.id !== orchestratorId && inScope(s)).map((s) => ({
|
|
@@ -113506,15 +114472,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113506
114472
|
const target = this.meta.get(sessionId);
|
|
113507
114473
|
if (!target || !inScope(target)) return { ok: false, error: scopeError(sessionId) };
|
|
113508
114474
|
try {
|
|
113509
|
-
const info = await this.updateSettings(sessionId, s);
|
|
113510
|
-
this.emit({
|
|
113511
|
-
type: "settings_changed",
|
|
113512
|
-
sessionId,
|
|
113513
|
-
model: info.model,
|
|
113514
|
-
effort: info.effort,
|
|
113515
|
-
verbosity: info.verbosity,
|
|
113516
|
-
serviceTier: info.serviceTier
|
|
113517
|
-
});
|
|
114475
|
+
const info = await this.updateSettings(sessionId, s, "orchestrator");
|
|
113518
114476
|
return { ok: true, ...info.applied === "after_turn" ? { deferred: true } : {} };
|
|
113519
114477
|
} catch (e) {
|
|
113520
114478
|
return { ok: false, error: e.message };
|
|
@@ -113670,8 +114628,8 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113670
114628
|
sessionId: orchestratorId,
|
|
113671
114629
|
name: orch?.name ?? "Orchestrator"
|
|
113672
114630
|
});
|
|
113673
|
-
if (reportBack) this.
|
|
113674
|
-
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);
|
|
113675
114633
|
return { ok: true };
|
|
113676
114634
|
} catch (e) {
|
|
113677
114635
|
return { ok: false, error: e.message };
|
|
@@ -113936,15 +114894,19 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113936
114894
|
if (boundary) console.error(`[permissions] auto session ${sessionId}: ${boundary} allowed`);
|
|
113937
114895
|
return true;
|
|
113938
114896
|
}
|
|
113939
|
-
/**
|
|
113940
|
-
|
|
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 = {}) {
|
|
113941
114903
|
if (signal?.aborted) return Promise.resolve(null);
|
|
113942
114904
|
const note = detail.kind === "project_access" && !detail.to.trusted ? " (target not trusted)" : "";
|
|
113943
114905
|
if (this.autoBoundaryOpen(sessionId, `${detail.kind}${note}`)) return Promise.resolve("allow");
|
|
113944
114906
|
return new Promise((resolve12) => {
|
|
113945
114907
|
if (signal?.aborted) return resolve12(null);
|
|
113946
|
-
const requestId = `${detail.kind === "project_access" ? "xp" : detail.kind === "agent_start" ? "ag" : detail.kind === "centralu_update" ? "up" : "mk"}-${randomUUID10()}`;
|
|
113947
|
-
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 });
|
|
113948
114910
|
signal?.addEventListener("abort", () => {
|
|
113949
114911
|
if (!this.hostAsks.delete(requestId)) return;
|
|
113950
114912
|
const m = this.meta.get(sessionId);
|
|
@@ -114141,17 +115103,20 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114141
115103
|
this.delegations.set(key, d2);
|
|
114142
115104
|
return d2;
|
|
114143
115105
|
}
|
|
114144
|
-
/**
|
|
114145
|
-
|
|
114146
|
-
|
|
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);
|
|
114147
115112
|
const shared = active && !active.finished ? active : void 0;
|
|
114148
|
-
|
|
114149
|
-
|
|
114150
|
-
|
|
114151
|
-
() =>
|
|
114152
|
-
|
|
114153
|
-
|
|
114154
|
-
);
|
|
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)));
|
|
114155
115120
|
const project2 = this.store.listProjects().find((p) => p.id === child.projectId);
|
|
114156
115121
|
const target = { id: child.projectId ?? "", name: project2?.name ?? "", path: this.store.sessionCwd(child.id) ?? project2?.path ?? "" };
|
|
114157
115122
|
if (!shared && subscribe) {
|
|
@@ -114249,7 +115214,8 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114249
115214
|
* 1. Who may: an ordinary session of a trusted project (the rule ask_project and attach_app follow, #382), and
|
|
114250
115215
|
* not one that was itself started or asked by another session (`askedBy`): depth one.
|
|
114251
115216
|
* 2. How many: at most `AGENT_CEILING` children run at once in a project, over all its sessions.
|
|
114252
|
-
* 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.
|
|
114253
115219
|
* 4. The child: a visible session in the caller's folder, marked `askedBy` the caller, on the tool, model and
|
|
114254
115220
|
* effort asked for (or the project's own, as a session the person opens there), under **the caller's own
|
|
114255
115221
|
* permission preset**: never more than the caller has, and nothing the caller can change afterwards.
|
|
@@ -114258,49 +115224,184 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114258
115224
|
* "working" and the caller is told when the turn ends (the reportBack path); agent_session waits again.
|
|
114259
115225
|
*/
|
|
114260
115226
|
async startAgent(callerId, opts, signal) {
|
|
114261
|
-
const
|
|
114262
|
-
|
|
114263
|
-
|
|
114264
|
-
|
|
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);
|
|
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;
|
|
114265
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 });
|
|
114266
115277
|
const project2 = this.store.listProjects().find((p) => p.id === caller.projectId);
|
|
114267
|
-
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." });
|
|
114268
115279
|
if (project2.trusted !== true && !this.autoBoundaryOpen(callerId, "start in an untrusted project")) {
|
|
114269
|
-
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}` });
|
|
114270
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));
|
|
114271
115286
|
const tool = opts.tool ?? this.firstTool(project2.defaultTool);
|
|
114272
|
-
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." });
|
|
114273
115288
|
const adapter = this.adapters.get(tool);
|
|
114274
|
-
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"}.` });
|
|
114275
115290
|
const full = this.agentCeilingError(callerId, project2.id);
|
|
114276
|
-
if (full) return { ok: false, error: full };
|
|
115291
|
+
if (full) return done({ ok: false, error: full });
|
|
114277
115292
|
const remembered = this.store.projectToolDefaults(project2.id)[tool];
|
|
114278
115293
|
const model = opts.model ?? remembered?.model ?? void 0;
|
|
114279
115294
|
const effort = opts.effort ?? (!opts.model || opts.model === remembered?.model ? remembered?.effort ?? void 0 : void 0);
|
|
114280
|
-
const
|
|
114281
|
-
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;
|
|
114282
115299
|
const found = await adapter.detect();
|
|
114283
|
-
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}` });
|
|
114284
115301
|
const worktree = opts.worktree || opts.branch ? await this.agentWorktreeBranch(project2, opts) : null;
|
|
114285
|
-
if (worktree && "error" in worktree) return { ok: false, error: worktree.error };
|
|
114286
|
-
|
|
114287
|
-
|
|
114288
|
-
|
|
114289
|
-
|
|
114290
|
-
|
|
114291
|
-
|
|
114292
|
-
|
|
114293
|
-
|
|
114294
|
-
|
|
114295
|
-
|
|
114296
|
-
|
|
114297
|
-
|
|
114298
|
-
|
|
114299
|
-
|
|
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;
|
|
114300
115381
|
const fullNow = this.agentCeilingError(callerId, project2.id);
|
|
114301
|
-
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;
|
|
114302
115402
|
const cwd = worktree ? project2.path : this.store.sessionCwd(callerId) ?? caller.worktree?.path ?? project2.path;
|
|
114303
115403
|
let child;
|
|
115404
|
+
progress.stage = "creating";
|
|
114304
115405
|
try {
|
|
114305
115406
|
child = await this.createSession({
|
|
114306
115407
|
projectId: project2.id,
|
|
@@ -114318,21 +115419,24 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114318
115419
|
...worktree ? { worktree: true, worktreeBranch: worktree.branch, ...caller.worktree ? { worktreeBase: caller.worktree.branch } : {} } : {}
|
|
114319
115420
|
});
|
|
114320
115421
|
} catch (e) {
|
|
114321
|
-
return { ok: false, error: `Could not start the session: ${e.message}` };
|
|
115422
|
+
return done({ ok: false, error: `Could not start the session: ${e.message}` });
|
|
114322
115423
|
}
|
|
114323
|
-
|
|
115424
|
+
progress.stage = "created";
|
|
115425
|
+
progress.childId = child.id;
|
|
115426
|
+
if (!worktree) this.rename(child.id, agentName(choice.task));
|
|
114324
115427
|
const key = `${callerId}>${child.id}`;
|
|
114325
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;
|
|
114326
115431
|
try {
|
|
114327
|
-
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);
|
|
114328
115433
|
} catch (e) {
|
|
114329
115434
|
this.delegations.delete(key);
|
|
114330
115435
|
if (this.agentRuns.get(child.id) === d2.wait) this.agentRuns.delete(child.id);
|
|
114331
|
-
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}` });
|
|
114332
115437
|
}
|
|
114333
115438
|
const notice = this.handles.get(child.id)?.delegationNotice?.();
|
|
114334
|
-
|
|
114335
|
-
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 };
|
|
114336
115440
|
}
|
|
114337
115441
|
/**
|
|
114338
115442
|
* agent_session (#561): safe/normal work only with their own children; auto opens ownership within the project.
|
|
@@ -114342,19 +115446,34 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114342
115446
|
* send gives it more, including during the caller's turn on adapters that accept mid-turn input; refused
|
|
114343
115447
|
* while it works on the person's own message or has a higher preset than the caller
|
|
114344
115448
|
* stop stops the turn the caller gave it; a turn the person started there is theirs
|
|
114345
|
-
* delete
|
|
115449
|
+
* delete trashes a child without a worktree, stopping it first; worktree cleanup requires an idle child (#608)
|
|
114346
115450
|
*/
|
|
114347
115451
|
async agentSession(callerId, opts, signal) {
|
|
114348
115452
|
if (opts.sessionId === callerId && opts.action !== "status") {
|
|
114349
115453
|
return { ok: false, error: "agent_session cannot wait on, stop, delete or send to its own calling session." };
|
|
114350
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
|
+
}
|
|
114351
115466
|
const caller = this.meta.get(callerId);
|
|
114352
115467
|
const child = this.meta.get(opts.sessionId);
|
|
114353
|
-
|
|
115468
|
+
const deletingWithoutWorktree = opts.action === "delete" && !child?.worktree;
|
|
115469
|
+
const startedHere = child?.askedBy === callerId || deletingWithoutWorktree && child?.parentSessionId === callerId;
|
|
115470
|
+
if (!caller?.projectId || !child || !startedHere && (deletingWithoutWorktree || !this.autoBoundaryOpen(callerId, "session ownership")) || child.projectId !== caller.projectId) {
|
|
114354
115471
|
return { ok: false, error: `${opts.sessionId} is not a session this session started. agent_session works only on sessions start_agent started here.` };
|
|
114355
115472
|
}
|
|
114356
115473
|
if (opts.sessionId === callerId) return this.agentAtRest(child);
|
|
114357
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);
|
|
114358
115477
|
let d2 = this.delegations.get(key);
|
|
114359
115478
|
let releaseObservation;
|
|
114360
115479
|
if (!d2 && this.autoBoundaryOpen(callerId)) {
|
|
@@ -114376,13 +115495,22 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114376
115495
|
return d2 ? await this.awaitAgent(key, d2, signal, observing) : this.agentAtRest(child);
|
|
114377
115496
|
case "stop": {
|
|
114378
115497
|
if (!d2 || d2.settled) return { ok: false, error: `Nothing you gave "${child.name}" is running.` };
|
|
114379
|
-
if (!observing && this.awaitingReport.get(child.id) === callerId) this.
|
|
115498
|
+
if (!observing && this.awaitingReport.get(child.id) === callerId) this.clearReport(child.id);
|
|
114380
115499
|
d2.stop("the session that started it stopped it", observing);
|
|
114381
115500
|
await d2.outcome;
|
|
114382
115501
|
if (!observing) this.delegations.delete(key);
|
|
114383
115502
|
return { ok: true, state: "stopped", sessionId: child.id, sessionName: child.name, tool: child.tool };
|
|
114384
115503
|
}
|
|
114385
115504
|
case "delete": {
|
|
115505
|
+
if (!child.worktree) {
|
|
115506
|
+
const busy = child.state === "working" || child.state === "waiting_approval" || !!child.pendingApproval || child.pendingQuestions.length > 0 || this.agentRuns.has(child.id) && !this.agentRuns.get(child.id).finished;
|
|
115507
|
+
if (!this.trashing.has(child.id) && busy && (!d2 || d2.settled || d2.callerId !== callerId)) {
|
|
115508
|
+
return { ok: false, error: `"${child.name}" is busy with something else; the person may be using it. Wait for that turn and its pending approval or question to finish before deleting it.` };
|
|
115509
|
+
}
|
|
115510
|
+
await this.trashSession(child.id);
|
|
115511
|
+
if (d2) this.delegations.delete(key);
|
|
115512
|
+
return { ok: true, state: "deleted", sessionId: child.id, sessionName: child.name, tool: child.tool };
|
|
115513
|
+
}
|
|
114386
115514
|
if (d2 && !d2.settled) return { ok: false, error: `"${child.name}" is still working on what you gave it \u2014 stop it or wait first.` };
|
|
114387
115515
|
const r = await this.cleanUpWorktreeChild(child);
|
|
114388
115516
|
if (!r.ok) return r;
|
|
@@ -114402,7 +115530,12 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114402
115530
|
if (!this.adapters.get(child.tool)?.capabilities.midTurnInput || !this.handles.get(child.id)?.sendTracked) {
|
|
114403
115531
|
return { ok: false, error: `"${child.name}" cannot take messages while working. Wait for it (action "wait") or stop it first.` };
|
|
114404
115532
|
}
|
|
114405
|
-
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();
|
|
114406
115539
|
const release = () => {
|
|
114407
115540
|
releaseHold();
|
|
114408
115541
|
this.applyDeferredRestart(child.id);
|
|
@@ -114410,12 +115543,24 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114410
115543
|
try {
|
|
114411
115544
|
await this.deliver(child.id, text2, void 0, from, false, void 0, "inline", (h, input) => {
|
|
114412
115545
|
const fail4 = (e) => {
|
|
114413
|
-
|
|
115546
|
+
if (!record2) {
|
|
115547
|
+
wait.fail(e);
|
|
115548
|
+
return release();
|
|
115549
|
+
}
|
|
115550
|
+
wait.fail(new Error(`your note was not delivered: ${e.message}`));
|
|
114414
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
|
+
}
|
|
114415
115556
|
};
|
|
114416
115557
|
void h.sendTracked(input, { complete: release, fail: fail4 }).then(release, fail4);
|
|
114417
115558
|
});
|
|
114418
115559
|
} catch (e) {
|
|
115560
|
+
if (record2) {
|
|
115561
|
+
wait.fail(e);
|
|
115562
|
+
if (this.observerNotes.get(key) === record2) this.observerNotes.delete(key);
|
|
115563
|
+
}
|
|
114419
115564
|
release();
|
|
114420
115565
|
return { ok: false, error: `Could not send it: ${e.message}` };
|
|
114421
115566
|
}
|
|
@@ -114446,6 +115591,44 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114446
115591
|
releaseObservation?.();
|
|
114447
115592
|
}
|
|
114448
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
|
+
}
|
|
114449
115632
|
/**
|
|
114450
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
|
|
114451
115634
|
* named from the job (`agentBranch`). Refused before the card when the project is no git repository, so the person
|
|
@@ -114458,8 +115641,22 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114458
115641
|
if (asked) return await gitValidBranchName(asked) ? { branch: asked } : { error: `Not a valid branch name: ${asked}` };
|
|
114459
115642
|
return { branch: agentBranch(opts.task, randomUUID10()) };
|
|
114460
115643
|
}
|
|
114461
|
-
/** Prevent a send/resume from racing the snapshot and worktree removal
|
|
114462
|
-
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
|
+
}
|
|
114463
115660
|
/** #608: the parent decides; local work follows the session into the trash in every preset. */
|
|
114464
115661
|
async cleanUpWorktreeChild(target) {
|
|
114465
115662
|
if (!target.worktree?.base || !target.projectId) {
|
|
@@ -114468,10 +115665,21 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114468
115665
|
if (target.state === "working" || target.state === "waiting_approval" || this.agentRuns.has(target.id) || sessionBusy({ ...target, live: this.handles.has(target.id) })) {
|
|
114469
115666
|
return { ok: false, error: `"${target.name}" is still working \u2014 stop it or wait first` };
|
|
114470
115667
|
}
|
|
115668
|
+
if (!existsSync19(target.worktree.path)) {
|
|
115669
|
+
return { ok: false, error: `The worktree folder for "${target.name}" is missing: ${target.worktree.path}. Restore the folder before cleanup; its branch has been left in place.` };
|
|
115670
|
+
}
|
|
114471
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` };
|
|
114472
115672
|
if (this.worktreeCleanup.has(target.id)) return { ok: false, error: "This worktree is already being removed" };
|
|
114473
|
-
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;
|
|
114474
115681
|
const { branch, path: path2 } = target.worktree;
|
|
115682
|
+
this.worktreeCleanup.set(target.id, canonicalPath(path2));
|
|
114475
115683
|
try {
|
|
114476
115684
|
const cwd = this.cwdOf(target.projectId);
|
|
114477
115685
|
const handle = this.handles.get(target.id);
|
|
@@ -114492,7 +115700,11 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114492
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.`
|
|
114493
115701
|
});
|
|
114494
115702
|
}
|
|
115703
|
+
const appeared = occupied();
|
|
115704
|
+
if (appeared) return appeared;
|
|
114495
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)}.` };
|
|
114496
115708
|
try {
|
|
114497
115709
|
await gitWorktreeRemove(cwd, path2, true, true);
|
|
114498
115710
|
} catch (e) {
|
|
@@ -114500,6 +115712,8 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114500
115712
|
}
|
|
114501
115713
|
try {
|
|
114502
115714
|
await gitBranchDelete(cwd, branch, this.gitTrust(target.projectId));
|
|
115715
|
+
await clearRecoveredBranchMark(cwd, branch).catch(() => {
|
|
115716
|
+
});
|
|
114503
115717
|
} catch (e) {
|
|
114504
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)}.` };
|
|
114505
115719
|
}
|
|
@@ -114510,12 +115724,27 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114510
115724
|
this.worktreeCleanup.delete(target.id);
|
|
114511
115725
|
}
|
|
114512
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
|
+
}
|
|
114513
115742
|
/** A saved ceiling overrides the caller's preset default: auto unlimited, otherwise AGENT_CEILING. */
|
|
114514
115743
|
agentCeilingError(callerId, projectId) {
|
|
114515
115744
|
const configured = this.uiPreferences().agentCeiling;
|
|
114516
115745
|
const ceiling = configured ?? (this.autoBoundaryOpen(callerId) ? 0 : AGENT_CEILING);
|
|
114517
115746
|
if (ceiling === 0) return null;
|
|
114518
|
-
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);
|
|
114519
115748
|
if (running < ceiling) return null;
|
|
114520
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.`;
|
|
114521
115750
|
}
|
|
@@ -114525,22 +115754,38 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114525
115754
|
* list's ids or labels contain (an alias such as "sonnet") passes, and a tool that cannot list its models passes all.
|
|
114526
115755
|
*/
|
|
114527
115756
|
async agentChoiceError(tool, askedModel, askedEffort, model) {
|
|
114528
|
-
if (!askedModel && !askedEffort) return null;
|
|
114529
|
-
const
|
|
114530
|
-
if (!
|
|
114531
|
-
const
|
|
114532
|
-
|
|
114533
|
-
|
|
114534
|
-
|
|
114535
|
-
|
|
114536
|
-
|
|
114537
|
-
|
|
114538
|
-
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);
|
|
114539
115783
|
}
|
|
114540
|
-
return
|
|
115784
|
+
return { error: wrong };
|
|
114541
115785
|
}
|
|
114542
115786
|
/** Update grants last only for the calling session on this host; they never change automatic application. */
|
|
114543
|
-
async updateCentraluAsked(sessionId, action) {
|
|
115787
|
+
async updateCentraluAsked(sessionId, action, signal) {
|
|
115788
|
+
if (action === "apply") return this.applyUpdateAsked(sessionId, signal);
|
|
114544
115789
|
const updates2 = this.updates;
|
|
114545
115790
|
if (!updates2) return { ok: false, error: "Update checks are unavailable" };
|
|
114546
115791
|
if (this.toolProfileOf(sessionId) === "reader" && !this.updateConsents.has(sessionId)) {
|
|
@@ -114548,19 +115793,71 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114548
115793
|
if (decision === "always") this.updateConsents.add(sessionId);
|
|
114549
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." };
|
|
114550
115795
|
}
|
|
114551
|
-
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 } : {} };
|
|
114552
115829
|
}
|
|
114553
115830
|
/**
|
|
114554
115831
|
* The person's consent for a session to start agents (#561), the first time it does: a card in the caller's approval
|
|
114555
115832
|
* slot. "Always" is kept for this session while this host serves it; with ⌥ (scope `project`) it is remembered as the
|
|
114556
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.
|
|
114557
115839
|
*/
|
|
114558
|
-
async ensureAgentConsent(callerId, detail, signal) {
|
|
115840
|
+
async ensureAgentConsent(callerId, detail, signal, onCard) {
|
|
114559
115841
|
if (this.agentConsents.has(callerId)) return { ok: true };
|
|
114560
115842
|
if (this.store.getProjectConsent(detail.project.id, detail.project.id, "agents")) return { ok: true };
|
|
114561
|
-
|
|
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) {
|
|
114562
115859
|
if (decision === "always_project") {
|
|
114563
|
-
this.store.setProjectConsent(
|
|
115860
|
+
this.store.setProjectConsent(projectId, projectId, "agents");
|
|
114564
115861
|
this.emit({ type: "project_consents_changed" });
|
|
114565
115862
|
return { ok: true };
|
|
114566
115863
|
}
|
|
@@ -114569,15 +115866,112 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114569
115866
|
return { ok: true };
|
|
114570
115867
|
}
|
|
114571
115868
|
if (decision === "allow") return { ok: true };
|
|
114572
|
-
if (decision === "deny")
|
|
114573
|
-
|
|
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
|
+
})();
|
|
114574
115968
|
}
|
|
114575
115969
|
/**
|
|
114576
115970
|
* "Working": the caller is told when the turn ends (#561), through the report the orchestrator's reportBack uses, unless
|
|
114577
115971
|
* a call is waiting on it by then (`awaitAgent` takes the marker down while it waits)
|
|
114578
115972
|
*/
|
|
114579
115973
|
agentWorking(d2, arm = true, observing = false) {
|
|
114580
|
-
if (arm && !observing) this.
|
|
115974
|
+
if (arm && !observing) this.markReport(d2.sessionId, d2.callerId);
|
|
114581
115975
|
const notice = d2.notice();
|
|
114582
115976
|
return {
|
|
114583
115977
|
ok: true,
|
|
@@ -114589,6 +115983,43 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114589
115983
|
...notice ? { notice } : {}
|
|
114590
115984
|
};
|
|
114591
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
|
+
}
|
|
114592
116023
|
/** A child with nothing of the caller's running: its last final answer (#561) */
|
|
114593
116024
|
agentAtRest(child) {
|
|
114594
116025
|
return {
|
|
@@ -114602,12 +116033,12 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114602
116033
|
};
|
|
114603
116034
|
}
|
|
114604
116035
|
/** Waits on a child's turn up to the bound (#561), like `awaitDelegation`, and reads its outcome for the caller */
|
|
114605
|
-
async awaitAgent(key, d2, signal, observing = false) {
|
|
114606
|
-
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);
|
|
114607
116038
|
const name = () => this.meta.get(d2.sessionId)?.name ?? d2.sessionId;
|
|
114608
116039
|
let timer;
|
|
114609
116040
|
let onAbort;
|
|
114610
|
-
const bound = new Promise((resolve12) => timer = setTimeout(() => resolve12("bound"),
|
|
116041
|
+
const bound = new Promise((resolve12) => timer = setTimeout(() => resolve12("bound"), boundMs));
|
|
114611
116042
|
const aborted2 = new Promise((resolve12) => {
|
|
114612
116043
|
if (!signal) return;
|
|
114613
116044
|
onAbort = () => resolve12("aborted");
|
|
@@ -114710,6 +116141,7 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114710
116141
|
const next = [...this.hostAsks.values()].find((a) => a.sessionId === sessionId);
|
|
114711
116142
|
if (!next) return;
|
|
114712
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;
|
|
114713
116145
|
if (!next.shown) {
|
|
114714
116146
|
next.shown = true;
|
|
114715
116147
|
this.onEvent(e);
|
|
@@ -114719,6 +116151,19 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114719
116151
|
this.store.upsertSession(m);
|
|
114720
116152
|
this.emit(e);
|
|
114721
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
|
+
}
|
|
114722
116167
|
/** Screen-originated capability questions still awaiting an answer (M4 D-4) */
|
|
114723
116168
|
appQuestionList() {
|
|
114724
116169
|
return [...this.appQuestions.values()].map((x) => ({ ...x.question })).sort((a, b2) => a.askedAt - b2.askedAt);
|
|
@@ -114961,8 +116406,7 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114961
116406
|
if (!profileAllows(profile, name)) {
|
|
114962
116407
|
throw Object.assign(new Error(`Not a tool of this session: ${name}`), { code: "internal" });
|
|
114963
116408
|
}
|
|
114964
|
-
|
|
114965
|
-
return runOrchestratorTool(tools, name, args, { sessionId, profile });
|
|
116409
|
+
return runOrchestratorTool(this.toolBundleOf(sessionId, profile, m), name, args, { sessionId, profile });
|
|
114966
116410
|
}
|
|
114967
116411
|
/**
|
|
114968
116412
|
* Receives the external app runtime (M4 A-5) — a session that comes up after this receives whatever
|
|
@@ -114980,6 +116424,10 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
114980
116424
|
useUpdates(updates2) {
|
|
114981
116425
|
this.updates = updates2;
|
|
114982
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
|
+
}
|
|
114983
116431
|
/**
|
|
114984
116432
|
* Says in the conversation that the session moved to a newer CLI (#297): restarting a process by itself is never
|
|
114985
116433
|
* silent. A notice from Centralu, not from the tool, so it reads as what happened rather than as something to fix.
|
|
@@ -115124,41 +116572,33 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
115124
116572
|
if (this.isAppAgentSession(m)) return void 0;
|
|
115125
116573
|
return this.appsHub?.attach({ id: m.id, kind: m.kind, projectId: m.projectId, builderOf: this.builderRefOf(m) }) ?? void 0;
|
|
115126
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
|
+
}
|
|
115127
116579
|
/**
|
|
115128
|
-
*
|
|
115129
|
-
*
|
|
115130
|
-
*
|
|
115131
|
-
* 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.
|
|
115132
116583
|
*/
|
|
115133
|
-
|
|
115134
|
-
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 };
|
|
115135
116586
|
}
|
|
115136
116587
|
/**
|
|
115137
|
-
* The tool bundle this session receives (#69). null means no tools.
|
|
115138
|
-
*
|
|
115139
|
-
*
|
|
115140
|
-
* 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.
|
|
115141
116591
|
*/
|
|
115142
116592
|
toolProfileOf(sessionId) {
|
|
115143
116593
|
const m = this.meta.get(sessionId);
|
|
115144
|
-
|
|
115145
|
-
if (m.kind === "orchestrator") return "orchestrator";
|
|
115146
|
-
if (m.kind === "coordinator") return "scoped";
|
|
115147
|
-
if (this.builderRefOf(m)) return "builder";
|
|
115148
|
-
return this.readsOwnProject(m) ? "reader" : null;
|
|
116594
|
+
return m ? toolProfileFor(this.profileFacts(m)) : null;
|
|
115149
116595
|
}
|
|
115150
|
-
|
|
115151
|
-
|
|
115152
|
-
|
|
115153
|
-
|
|
115154
|
-
|
|
115155
|
-
|
|
115156
|
-
* The schemas are attached when the session starts, like every other bundle; turning the setting off
|
|
115157
|
-
* stops the calls at once (readerToolsFor and the bridge both ask again) and takes the schemas away
|
|
115158
|
-
* the next time the session starts.
|
|
115159
|
-
*/
|
|
115160
|
-
readsOwnProject(m) {
|
|
115161
|
-
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
|
+
});
|
|
115162
116602
|
}
|
|
115163
116603
|
/**
|
|
115164
116604
|
* A profile that directs other sessions — every profile but the reader. The reader set (#320)
|
|
@@ -115355,8 +116795,8 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
115355
116795
|
return readMessageImage(abs);
|
|
115356
116796
|
}
|
|
115357
116797
|
repliesName(sessionId, written) {
|
|
115358
|
-
const
|
|
115359
|
-
if (
|
|
116798
|
+
const open6 = this.streams.get(sessionId);
|
|
116799
|
+
if (open6?.kind === "text" && mentions(open6.text, written)) return true;
|
|
115360
116800
|
return writtenForms(written).some((form) => this.store.replyTextsContaining(sessionId, form).some((text2) => mentions(text2, written)));
|
|
115361
116801
|
}
|
|
115362
116802
|
/**
|
|
@@ -115402,6 +116842,7 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
115402
116842
|
);
|
|
115403
116843
|
}
|
|
115404
116844
|
async disposeAll() {
|
|
116845
|
+
this.disposing = true;
|
|
115405
116846
|
this.watchers.close();
|
|
115406
116847
|
for (const readers of this.turnObservers.values()) for (const observer of readers) observer.fail(new Error("Centralu is stopping"));
|
|
115407
116848
|
this.appsHub?.rt.attachBrokerHost(null);
|
|
@@ -115433,6 +116874,7 @@ ${list()}` : `${target.name} has no ordinary session. The person has to open one
|
|
|
115433
116874
|
async detachAll() {
|
|
115434
116875
|
this.watchers.close();
|
|
115435
116876
|
this.appsHub?.rt.attachBrokerHost(null);
|
|
116877
|
+
this.handingOver = true;
|
|
115436
116878
|
for (const run of [...this.agentRuns.values()]) run.fail(new Error("Centralu is restarting"));
|
|
115437
116879
|
for (const readers of this.turnObservers.values()) for (const observer of readers) observer.fail(new Error("Centralu is restarting"));
|
|
115438
116880
|
for (const ask of [...this.hostAsks.values()]) ask.resolve(null);
|
|
@@ -115466,7 +116908,7 @@ import { relative as relative9 } from "node:path";
|
|
|
115466
116908
|
// packages/agent-host/src/themes.ts
|
|
115467
116909
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
115468
116910
|
import { watch as watch2 } from "node:fs";
|
|
115469
|
-
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";
|
|
115470
116912
|
import { basename as basename6, join as join30 } from "node:path";
|
|
115471
116913
|
var MAX_FILE_BYTES = 256 * 1024;
|
|
115472
116914
|
var SETTLE_MS = 100;
|
|
@@ -115580,7 +117022,7 @@ var ThemeFiles = class {
|
|
|
115580
117022
|
const tmp = `${path2}.${randomBytes4(6).toString("hex")}.tmp`;
|
|
115581
117023
|
try {
|
|
115582
117024
|
await writeFile5(tmp, text2, "utf8");
|
|
115583
|
-
await
|
|
117025
|
+
await rename5(tmp, path2);
|
|
115584
117026
|
} catch (e) {
|
|
115585
117027
|
await unlink2(tmp).catch(() => {
|
|
115586
117028
|
});
|
|
@@ -115591,7 +117033,7 @@ var ThemeFiles = class {
|
|
|
115591
117033
|
|
|
115592
117034
|
// packages/agent-host/src/dev-services/strays.ts
|
|
115593
117035
|
import { execFile as execFile7 } from "node:child_process";
|
|
115594
|
-
import { realpathSync as
|
|
117036
|
+
import { realpathSync as realpathSync9 } from "node:fs";
|
|
115595
117037
|
import { promisify as promisify7 } from "node:util";
|
|
115596
117038
|
import { userInfo } from "node:os";
|
|
115597
117039
|
var exec7 = promisify7(execFile7);
|
|
@@ -115702,7 +117144,7 @@ function resolveRoots(roots) {
|
|
|
115702
117144
|
for (const r of roots) {
|
|
115703
117145
|
out.add(r);
|
|
115704
117146
|
try {
|
|
115705
|
-
out.add(
|
|
117147
|
+
out.add(realpathSync9(r));
|
|
115706
117148
|
} catch {
|
|
115707
117149
|
}
|
|
115708
117150
|
}
|
|
@@ -115815,7 +117257,7 @@ var toInfo = (h) => ({
|
|
|
115815
117257
|
function sessionIdsOf(panels) {
|
|
115816
117258
|
return panels.flatMap((p) => p.kind === "session" ? [p.sessionId] : []);
|
|
115817
117259
|
}
|
|
115818
|
-
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 } = {}) {
|
|
115819
117261
|
const requireThemes = () => {
|
|
115820
117262
|
if (!themes2) throw Object.assign(new Error("Theme files are unavailable"), { code: "internal" });
|
|
115821
117263
|
return themes2;
|
|
@@ -115928,8 +117370,8 @@ function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: upd
|
|
|
115928
117370
|
"agents.resumeSession": async (p) => mgr2.resumeSession(RpcMethods["agents.resumeSession"].params.parse(p).sessionId),
|
|
115929
117371
|
"agents.forkConversation": async (p) => mgr2.forkConversation(RpcMethods["agents.forkConversation"].params.parse(p).sessionId),
|
|
115930
117372
|
"agents.updateSettings": async (p) => {
|
|
115931
|
-
const { sessionId, ...settings } = RpcMethods["agents.updateSettings"].params.parse(p);
|
|
115932
|
-
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");
|
|
115933
117375
|
},
|
|
115934
117376
|
"agents.switchTool": async (p) => {
|
|
115935
117377
|
const { sessionId, tool } = RpcMethods["agents.switchTool"].params.parse(p);
|
|
@@ -116289,7 +117731,7 @@ function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: upd
|
|
|
116289
117731
|
},
|
|
116290
117732
|
"projects.setTrusted": async (p) => {
|
|
116291
117733
|
const { projectId, trusted } = RpcMethods["projects.setTrusted"].params.parse(p);
|
|
116292
|
-
mgr2.setProjectTrusted(projectId, trusted);
|
|
117734
|
+
void mgr2.setProjectTrusted(projectId, trusted).catch((err) => console.error("[sessions] restarting for a trust change failed:", err));
|
|
116293
117735
|
externalApps2?.refresh();
|
|
116294
117736
|
return { ok: true };
|
|
116295
117737
|
},
|
|
@@ -116477,6 +117919,16 @@ function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: upd
|
|
|
116477
117919
|
// Answers once the install has started, not once it has finished — see the note on
|
|
116478
117920
|
// `updates.apply` in the protocol. The rest arrives as `update_status` events.
|
|
116479
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
|
+
},
|
|
116480
117932
|
"host.stop": async () => {
|
|
116481
117933
|
if (!stopHost) {
|
|
116482
117934
|
throw Object.assign(new Error("This host runs under the Centralu app, which stops it: quit the app instead"), { code: "internal" });
|
|
@@ -116551,6 +118003,143 @@ function onExternalAppListChanged(apps, emit) {
|
|
|
116551
118003
|
});
|
|
116552
118004
|
}
|
|
116553
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
|
+
|
|
116554
118143
|
// packages/agent-host/src/app-change-events.ts
|
|
116555
118144
|
var APP_CHANGE_WINDOW_MS = 250;
|
|
116556
118145
|
function broadcastAppChanges(send, windowMs = APP_CHANGE_WINDOW_MS) {
|
|
@@ -116558,9 +118147,9 @@ function broadcastAppChanges(send, windowMs = APP_CHANGE_WINDOW_MS) {
|
|
|
116558
118147
|
return {
|
|
116559
118148
|
emit(ref, cause2 = null) {
|
|
116560
118149
|
const key = JSON.stringify([ref.projectId, ref.appId]);
|
|
116561
|
-
const
|
|
116562
|
-
if (
|
|
116563
|
-
if (!sameCause(
|
|
118150
|
+
const open6 = pending.get(key);
|
|
118151
|
+
if (open6) {
|
|
118152
|
+
if (!sameCause(open6.cause, cause2)) open6.cause = null;
|
|
116564
118153
|
return;
|
|
116565
118154
|
}
|
|
116566
118155
|
const timer = setTimeout(() => {
|
|
@@ -117020,12 +118609,16 @@ var CommandRunner = class {
|
|
|
117020
118609
|
};
|
|
117021
118610
|
|
|
117022
118611
|
// packages/agent-host/src/updates.ts
|
|
117023
|
-
import { execFile as execFile8 } from "node:child_process";
|
|
117024
|
-
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";
|
|
117025
118615
|
var REGISTRY_URL = `https://registry.npmjs.org/${APP_SLUG}/latest`;
|
|
117026
118616
|
var CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
117027
118617
|
var SCHEDULED_TIMEOUT_MS = 3e3;
|
|
117028
118618
|
var FORCED_TIMEOUT_MS = 8e3;
|
|
118619
|
+
var INSTALL_CHECK_MAX_AGE_MS = 6e4;
|
|
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];
|
|
117029
118622
|
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
117030
118623
|
var NPM_RETRY_DELAY_MS = 3e4;
|
|
117031
118624
|
var NPM_RETRY_WINDOW_MS = 10 * 60 * 1e3;
|
|
@@ -117033,6 +118626,16 @@ function existingInstalledCopy() {
|
|
|
117033
118626
|
const path2 = installedCopyPath();
|
|
117034
118627
|
return existsSync21(path2) ? path2 : null;
|
|
117035
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
|
+
}
|
|
117036
118639
|
async function fetchLatestFromRegistry(timeoutMs) {
|
|
117037
118640
|
try {
|
|
117038
118641
|
const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(timeoutMs) });
|
|
@@ -117072,7 +118675,13 @@ var UpdateService = class {
|
|
|
117072
118675
|
fetchLatest: deps.fetchLatest ?? fetchLatestFromRegistry,
|
|
117073
118676
|
run: deps.run ?? runCommand,
|
|
117074
118677
|
installedCopy: deps.installedCopy ?? existingInstalledCopy,
|
|
118678
|
+
installedCopyVersion: deps.installedCopyVersion ?? ((copy) => readInstalledCopyVersion(copy)),
|
|
117075
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
|
+
}),
|
|
117076
118685
|
// If no place to persist it is given, treat auto-update as on — the same default that main.ts
|
|
117077
118686
|
// passes in.
|
|
117078
118687
|
readAuto: deps.readAuto ?? (() => true),
|
|
@@ -117106,10 +118715,16 @@ var UpdateService = class {
|
|
|
117106
118715
|
}
|
|
117107
118716
|
publish;
|
|
117108
118717
|
status;
|
|
118718
|
+
stopped = false;
|
|
118719
|
+
installing = false;
|
|
117109
118720
|
timer = null;
|
|
117110
118721
|
deps;
|
|
117111
118722
|
/** One check at a time — the timer and the button can land together */
|
|
117112
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;
|
|
117113
118728
|
current() {
|
|
117114
118729
|
return { ...this.status };
|
|
117115
118730
|
}
|
|
@@ -117121,16 +118736,23 @@ var UpdateService = class {
|
|
|
117121
118736
|
* closed while releases happened.
|
|
117122
118737
|
*/
|
|
117123
118738
|
start() {
|
|
118739
|
+
this.stopped = false;
|
|
117124
118740
|
if (!this.status.auto) return;
|
|
117125
118741
|
void this.check(false);
|
|
117126
118742
|
this.startTimer();
|
|
117127
118743
|
}
|
|
118744
|
+
/** Shutdown (including a host swap): no asynchronous continuation may start more work. */
|
|
117128
118745
|
stop() {
|
|
118746
|
+
this.stopped = true;
|
|
118747
|
+
this.stopTimer();
|
|
118748
|
+
this.cancelRetry();
|
|
118749
|
+
}
|
|
118750
|
+
stopTimer() {
|
|
117129
118751
|
if (this.timer) clearInterval(this.timer);
|
|
117130
118752
|
this.timer = null;
|
|
117131
118753
|
}
|
|
117132
118754
|
startTimer() {
|
|
117133
|
-
this.
|
|
118755
|
+
this.stopTimer();
|
|
117134
118756
|
this.timer = setInterval(() => void this.check(false), CHECK_INTERVAL_MS);
|
|
117135
118757
|
this.timer.unref?.();
|
|
117136
118758
|
}
|
|
@@ -117146,7 +118768,7 @@ var UpdateService = class {
|
|
|
117146
118768
|
this.status = { ...this.status, auto: enabled };
|
|
117147
118769
|
this.deps.writeAuto(enabled);
|
|
117148
118770
|
if (!enabled) {
|
|
117149
|
-
this.
|
|
118771
|
+
this.stopTimer();
|
|
117150
118772
|
this.emit();
|
|
117151
118773
|
return this.current();
|
|
117152
118774
|
}
|
|
@@ -117167,13 +118789,46 @@ var UpdateService = class {
|
|
|
117167
118789
|
this.deps.writeAutoApply(enabled);
|
|
117168
118790
|
this.emit();
|
|
117169
118791
|
if (enabled) this.installIfAutomatic();
|
|
118792
|
+
else this.cancelRetry();
|
|
117170
118793
|
return this.current();
|
|
117171
118794
|
}
|
|
117172
|
-
/**
|
|
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
|
+
*/
|
|
117173
118801
|
installIfAutomatic() {
|
|
117174
118802
|
if (!this.status.autoApply || !this.status.newer || !this.status.latest) return;
|
|
117175
|
-
if (this.status.phase
|
|
117176
|
-
|
|
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;
|
|
117177
118832
|
}
|
|
117178
118833
|
/**
|
|
117179
118834
|
* Look at the registry, or answer with what we already know.
|
|
@@ -117183,32 +118838,36 @@ var UpdateService = class {
|
|
|
117183
118838
|
* failed, nothing about a background failure interrupts anyone.
|
|
117184
118839
|
*/
|
|
117185
118840
|
async check(force) {
|
|
118841
|
+
if (this.stopped || this.installing) return this.current();
|
|
117186
118842
|
if (this.inFlight) return this.inFlight;
|
|
117187
118843
|
if (!force && !this.status.auto) return this.current();
|
|
117188
118844
|
if (!force && this.status.checkedAt !== null && !this.isDue()) return this.current();
|
|
117189
118845
|
if (this.status.phase === "updating" || this.status.phase === "waiting_for_npm" || this.status.phase === "restart_required") return this.current();
|
|
117190
|
-
this.status
|
|
118846
|
+
const failure3 = this.status.phase === "failed" ? { error: this.status.error } : null;
|
|
118847
|
+
if (!failure3) this.status = { ...this.status, phase: "checking" };
|
|
117191
118848
|
this.emit();
|
|
117192
|
-
this.inFlight = this.runCheck(force);
|
|
118849
|
+
this.inFlight = this.runCheck(force, failure3);
|
|
117193
118850
|
try {
|
|
117194
118851
|
return await this.inFlight;
|
|
117195
118852
|
} finally {
|
|
117196
118853
|
this.inFlight = null;
|
|
117197
118854
|
}
|
|
117198
118855
|
}
|
|
117199
|
-
async runCheck(force) {
|
|
118856
|
+
async runCheck(force, failure3) {
|
|
117200
118857
|
const res = await this.deps.fetchLatest(force ? FORCED_TIMEOUT_MS : SCHEDULED_TIMEOUT_MS);
|
|
118858
|
+
if (this.stopped || this.installing) return this.current();
|
|
117201
118859
|
if (!res.ok) {
|
|
117202
|
-
this.status = { ...this.status, phase: "idle", error: res.reason };
|
|
118860
|
+
this.status = { ...this.status, phase: failure3 ? "failed" : "idle", error: failure3 ? failure3.error : res.reason };
|
|
117203
118861
|
this.emit();
|
|
117204
118862
|
return this.current();
|
|
117205
118863
|
}
|
|
118864
|
+
const latest = res.version;
|
|
117206
118865
|
this.status = {
|
|
117207
118866
|
...this.status,
|
|
117208
|
-
latest
|
|
117209
|
-
newer: isNewerVersion(
|
|
117210
|
-
phase: "idle",
|
|
117211
|
-
error: null,
|
|
118867
|
+
latest,
|
|
118868
|
+
newer: isNewerVersion(latest, this.status.current),
|
|
118869
|
+
phase: failure3 ? "failed" : "idle",
|
|
118870
|
+
error: failure3?.error ?? null,
|
|
117212
118871
|
checkedAt: this.deps.now()
|
|
117213
118872
|
};
|
|
117214
118873
|
this.emit();
|
|
@@ -117219,37 +118878,95 @@ var UpdateService = class {
|
|
|
117219
118878
|
* Install the newer version, then say so. **Does not restart the app** — that is the
|
|
117220
118879
|
* window's step (#352).
|
|
117221
118880
|
*
|
|
117222
|
-
*
|
|
117223
|
-
*
|
|
117224
|
-
*
|
|
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.
|
|
117225
118891
|
*/
|
|
117226
118892
|
apply() {
|
|
117227
|
-
if (this.
|
|
117228
|
-
|
|
117229
|
-
|
|
117230
|
-
|
|
117231
|
-
|
|
117232
|
-
return this.current();
|
|
117233
|
-
}
|
|
117234
|
-
this.status = { ...this.status, phase: "updating", error: null };
|
|
118893
|
+
if (this.stopped || this.installing || this.status.phase === "restart_required") return Promise.resolve(this.current());
|
|
118894
|
+
this.installing = true;
|
|
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 };
|
|
117235
118898
|
this.emit();
|
|
117236
|
-
void this.runApply(
|
|
117237
|
-
return this.current();
|
|
118899
|
+
return new Promise((settled) => void this.runApply(known, settled));
|
|
117238
118900
|
}
|
|
117239
|
-
async runApply(
|
|
118901
|
+
async runApply(known, settled) {
|
|
118902
|
+
let version2 = known;
|
|
117240
118903
|
let packageInstalled = false;
|
|
118904
|
+
let answered = false;
|
|
118905
|
+
const answer = () => {
|
|
118906
|
+
if (answered) return;
|
|
118907
|
+
answered = true;
|
|
118908
|
+
settled(this.current());
|
|
118909
|
+
};
|
|
117241
118910
|
const copy = this.deps.installedCopy();
|
|
117242
118911
|
try {
|
|
118912
|
+
await this.inFlight;
|
|
118913
|
+
if (this.stopped) return;
|
|
118914
|
+
const age2 = this.status.checkedAt === null ? Infinity : this.deps.now() - this.status.checkedAt;
|
|
118915
|
+
if (version2 === null || age2 < 0 || age2 >= INSTALL_CHECK_MAX_AGE_MS) {
|
|
118916
|
+
const fresh = await this.deps.fetchLatest(FORCED_TIMEOUT_MS);
|
|
118917
|
+
if (this.stopped) return;
|
|
118918
|
+
if (fresh.ok) {
|
|
118919
|
+
const now = this.deps.now();
|
|
118920
|
+
const storedAge = this.status.checkedAt === null ? Infinity : now - this.status.checkedAt;
|
|
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 };
|
|
118925
|
+
if (!this.status.newer) {
|
|
118926
|
+
this.status = { ...this.status, phase: "idle", error: null };
|
|
118927
|
+
return;
|
|
118928
|
+
}
|
|
118929
|
+
} else if (version2 === null) {
|
|
118930
|
+
this.status = { ...this.status, phase: "idle", error: fresh.reason };
|
|
118931
|
+
return;
|
|
118932
|
+
} else {
|
|
118933
|
+
this.status = { ...this.status, error: `Could not refresh latest (${fresh.reason}); using previously checked ${version2}.` };
|
|
118934
|
+
}
|
|
118935
|
+
}
|
|
118936
|
+
if (version2 === null) return;
|
|
118937
|
+
this.status = { ...this.status, phase: "updating" };
|
|
118938
|
+
this.emit();
|
|
118939
|
+
answer();
|
|
117243
118940
|
await this.installPackage(version2);
|
|
118941
|
+
if (this.stopped) return;
|
|
117244
118942
|
packageInstalled = true;
|
|
117245
|
-
if (copy)
|
|
117246
|
-
|
|
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
|
+
}
|
|
118952
|
+
if (this.stopped) return;
|
|
118953
|
+
this.status = { ...this.status, phase: "restart_required" };
|
|
118954
|
+
this.retry = null;
|
|
117247
118955
|
} catch (e) {
|
|
118956
|
+
if (this.stopped) return;
|
|
117248
118957
|
const reason = e.message;
|
|
117249
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;
|
|
117250
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() };
|
|
118962
|
+
} finally {
|
|
118963
|
+
this.installing = false;
|
|
118964
|
+
if (!this.stopped) {
|
|
118965
|
+
this.emit();
|
|
118966
|
+
if (this.status.phase === "failed") this.installIfAutomatic();
|
|
118967
|
+
}
|
|
118968
|
+
answer();
|
|
117251
118969
|
}
|
|
117252
|
-
this.emit();
|
|
117253
118970
|
}
|
|
117254
118971
|
/**
|
|
117255
118972
|
* A just-published release can be visible from `latest` before its package is on the npm edge
|
|
@@ -117259,16 +118976,20 @@ var UpdateService = class {
|
|
|
117259
118976
|
*/
|
|
117260
118977
|
async installPackage(version2) {
|
|
117261
118978
|
const started = this.deps.now();
|
|
118979
|
+
const notice = this.status.error;
|
|
117262
118980
|
for (; ; ) {
|
|
118981
|
+
if (this.stopped) return;
|
|
117263
118982
|
try {
|
|
117264
118983
|
await this.deps.run("npm", ["i", "-g", "--prefer-online", `${APP_SLUG}@${version2}`]);
|
|
117265
118984
|
return;
|
|
117266
118985
|
} catch (err) {
|
|
118986
|
+
if (this.stopped) return;
|
|
117267
118987
|
if (!npmHasNotServedVersion(err) || this.deps.now() - started >= NPM_RETRY_WINDOW_MS) throw err;
|
|
117268
|
-
this.status = { ...this.status, phase: "waiting_for_npm", error: `Waiting for npm to serve ${version2}` };
|
|
118988
|
+
this.status = { ...this.status, phase: "waiting_for_npm", error: `Waiting for npm to serve ${version2}${notice ? `. ${notice}` : ""}` };
|
|
117269
118989
|
this.emit();
|
|
117270
118990
|
await this.deps.wait(NPM_RETRY_DELAY_MS);
|
|
117271
|
-
|
|
118991
|
+
if (this.stopped) return;
|
|
118992
|
+
this.status = { ...this.status, phase: "updating", error: notice };
|
|
117272
118993
|
this.emit();
|
|
117273
118994
|
}
|
|
117274
118995
|
}
|
|
@@ -117284,6 +119005,9 @@ var UpdateService = class {
|
|
|
117284
119005
|
function npmHasNotServedVersion(err) {
|
|
117285
119006
|
return /(?:\bETARGET\b|\bnotarget\b)/i.test(err instanceof Error ? err.message : String(err));
|
|
117286
119007
|
}
|
|
119008
|
+
function retryDelay(failures) {
|
|
119009
|
+
return AUTO_RETRY_DELAYS_MS[Math.min(Math.max(failures, 1), AUTO_RETRY_DELAYS_MS.length) - 1];
|
|
119010
|
+
}
|
|
117287
119011
|
|
|
117288
119012
|
// packages/agent-host/src/agent-versions.ts
|
|
117289
119013
|
var CHECK_INTERVAL_MS2 = 10 * 60 * 1e3;
|
|
@@ -117626,16 +119350,16 @@ var SessionSleeper = class {
|
|
|
117626
119350
|
};
|
|
117627
119351
|
|
|
117628
119352
|
// packages/agent-host/src/dev-services/instance-lock.ts
|
|
117629
|
-
import { execFileSync as
|
|
117630
|
-
import { readFileSync as
|
|
117631
|
-
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";
|
|
117632
119356
|
import Database2 from "better-sqlite3";
|
|
117633
119357
|
var OWNERSHIP_FILE = "host-ownership.sqlite";
|
|
117634
119358
|
var held = /* @__PURE__ */ new Set();
|
|
117635
119359
|
function processStartTime(pid) {
|
|
117636
119360
|
if (!hasPs()) return null;
|
|
117637
119361
|
try {
|
|
117638
|
-
const out =
|
|
119362
|
+
const out = execFileSync5("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
117639
119363
|
encoding: "utf8",
|
|
117640
119364
|
timeout: 2e3,
|
|
117641
119365
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -117662,15 +119386,15 @@ function parseHolder(raw) {
|
|
|
117662
119386
|
function acquireInstanceLock(dbPath2, startOf = processStartTime) {
|
|
117663
119387
|
if (dbPath2 === ":memory:") return { ok: true, release: () => {
|
|
117664
119388
|
} };
|
|
117665
|
-
const lockPath =
|
|
119389
|
+
const lockPath = join32(dirname11(dbPath2), "host.lock");
|
|
117666
119390
|
const readHolder = () => {
|
|
117667
119391
|
try {
|
|
117668
|
-
return parseHolder(
|
|
119392
|
+
return parseHolder(readFileSync14(lockPath, "utf8"));
|
|
117669
119393
|
} catch {
|
|
117670
119394
|
return null;
|
|
117671
119395
|
}
|
|
117672
119396
|
};
|
|
117673
|
-
const owner = takeOwnership(
|
|
119397
|
+
const owner = takeOwnership(join32(dirname11(dbPath2), OWNERSHIP_FILE));
|
|
117674
119398
|
if (!owner) {
|
|
117675
119399
|
const holder = readHolder();
|
|
117676
119400
|
return { ok: false, heldByPid: holder && Number.isInteger(holder.pid) && holder.pid > 0 ? holder.pid : null, lockPath };
|
|
@@ -117683,7 +119407,7 @@ function acquireInstanceLock(dbPath2, startOf = processStartTime) {
|
|
|
117683
119407
|
if (released) return;
|
|
117684
119408
|
released = true;
|
|
117685
119409
|
try {
|
|
117686
|
-
if (parseHolder(
|
|
119410
|
+
if (parseHolder(readFileSync14(lockPath, "utf8")).pid === process.pid) unlinkSync3(lockPath);
|
|
117687
119411
|
} catch {
|
|
117688
119412
|
}
|
|
117689
119413
|
giveUp(owner);
|
|
@@ -118098,7 +119822,7 @@ async function runDrain(bound, deps) {
|
|
|
118098
119822
|
}
|
|
118099
119823
|
|
|
118100
119824
|
// packages/agent-host/src/links/install.ts
|
|
118101
|
-
import { existsSync as existsSync22, readFileSync as
|
|
119825
|
+
import { existsSync as existsSync22, readFileSync as readFileSync15 } from "node:fs";
|
|
118102
119826
|
import { hostname as hostname3 } from "node:os";
|
|
118103
119827
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
118104
119828
|
import { gzipSync } from "node:zlib";
|
|
@@ -118159,7 +119883,7 @@ async function verifiedPackage(name, version2, opts = {}) {
|
|
|
118159
119883
|
}
|
|
118160
119884
|
|
|
118161
119885
|
// packages/agent-host/src/links/tunnel.ts
|
|
118162
|
-
import { spawn as
|
|
119886
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
118163
119887
|
import { createConnection as createConnection2, createServer as createServer3 } from "node:net";
|
|
118164
119888
|
function parseConnectionLine(stdout) {
|
|
118165
119889
|
const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
@@ -118423,7 +120147,7 @@ var SshTunnel = class {
|
|
|
118423
120147
|
}
|
|
118424
120148
|
run(args, stdin) {
|
|
118425
120149
|
return new Promise((resolve12, reject) => {
|
|
118426
|
-
const child =
|
|
120150
|
+
const child = spawn6(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: [stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"] });
|
|
118427
120151
|
if (stdin !== void 0) {
|
|
118428
120152
|
child.stdin.on("error", () => {
|
|
118429
120153
|
});
|
|
@@ -118482,7 +120206,7 @@ var SshTunnel = class {
|
|
|
118482
120206
|
this.opts.target,
|
|
118483
120207
|
...keepAlive ? [keepAlive] : []
|
|
118484
120208
|
];
|
|
118485
|
-
const child =
|
|
120209
|
+
const child = spawn6(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: ["ignore", "ignore", "pipe"] });
|
|
118486
120210
|
this.child = child;
|
|
118487
120211
|
let stderr = "";
|
|
118488
120212
|
child.stderr.on("data", (d2) => stderr += String(d2));
|
|
@@ -118776,7 +120500,7 @@ async function installRemote(o) {
|
|
|
118776
120500
|
function bundled(name, source) {
|
|
118777
120501
|
for (const url2 of [new URL(`./${name}`, import.meta.url), new URL(source, import.meta.url)]) {
|
|
118778
120502
|
const file2 = fileURLToPath6(url2);
|
|
118779
|
-
if (existsSync22(file2)) return
|
|
120503
|
+
if (existsSync22(file2)) return readFileSync15(file2, "utf8");
|
|
118780
120504
|
}
|
|
118781
120505
|
throw new Error(`${name} is missing from this host`);
|
|
118782
120506
|
}
|
|
@@ -118834,7 +120558,7 @@ async function removeAutostart(c) {
|
|
|
118834
120558
|
}
|
|
118835
120559
|
|
|
118836
120560
|
// packages/agent-host/src/links/probe.ts
|
|
118837
|
-
import { spawn as
|
|
120561
|
+
import { spawn as spawn7 } from "node:child_process";
|
|
118838
120562
|
var PROBE_MARK = "CENTRALU-PROBE";
|
|
118839
120563
|
var PROBE_PS = [
|
|
118840
120564
|
"$ErrorActionPreference = 'SilentlyContinue'",
|
|
@@ -118889,7 +120613,7 @@ function probeTarget(target, opts = {}) {
|
|
|
118889
120613
|
clearTimeout(timer);
|
|
118890
120614
|
resolve12(parseProbe(t, run));
|
|
118891
120615
|
};
|
|
118892
|
-
const child =
|
|
120616
|
+
const child = spawn7(opts.ssh ?? "ssh", args, { env: opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
118893
120617
|
const timer = setTimeout(() => {
|
|
118894
120618
|
child.kill("SIGTERM");
|
|
118895
120619
|
finish2({ code: 255, stdout: "", stderr: `ssh to ${t} did not answer in time` });
|
|
@@ -118902,10 +120626,10 @@ function probeTarget(target, opts = {}) {
|
|
|
118902
120626
|
}
|
|
118903
120627
|
|
|
118904
120628
|
// packages/agent-host/src/links/ssh-config.ts
|
|
118905
|
-
import { spawn as
|
|
118906
|
-
import { readdirSync as readdirSync8, readFileSync as
|
|
120629
|
+
import { spawn as spawn8 } from "node:child_process";
|
|
120630
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync16, statSync as statSync13 } from "node:fs";
|
|
118907
120631
|
import { homedir as homedir13 } from "node:os";
|
|
118908
|
-
import { isAbsolute as isAbsolute10, join as
|
|
120632
|
+
import { isAbsolute as isAbsolute10, join as join33, normalize, parse as parse6, sep as sep12 } from "node:path";
|
|
118909
120633
|
var MAX_DEPTH2 = 16;
|
|
118910
120634
|
function splitConfigLine(raw) {
|
|
118911
120635
|
const m = /^\s*([^\s=#]+)\s*(?:=\s*)?([\s\S]*)$/.exec(raw);
|
|
@@ -118971,7 +120695,7 @@ function expandGlob(path2) {
|
|
|
118971
120695
|
const next = [];
|
|
118972
120696
|
for (const base2 of found) {
|
|
118973
120697
|
if (!MAGIC.test(seg)) {
|
|
118974
|
-
next.push(
|
|
120698
|
+
next.push(join33(base2, seg));
|
|
118975
120699
|
continue;
|
|
118976
120700
|
}
|
|
118977
120701
|
let names;
|
|
@@ -118983,7 +120707,7 @@ function expandGlob(path2) {
|
|
|
118983
120707
|
const re2 = segmentRe(seg);
|
|
118984
120708
|
for (const name of names.sort()) {
|
|
118985
120709
|
if (name.startsWith(".") && !seg.startsWith(".")) continue;
|
|
118986
|
-
if (re2.test(name)) next.push(
|
|
120710
|
+
if (re2.test(name)) next.push(join33(base2, name));
|
|
118987
120711
|
}
|
|
118988
120712
|
}
|
|
118989
120713
|
found = next;
|
|
@@ -119013,7 +120737,7 @@ function readSshConfig(opts) {
|
|
|
119013
120737
|
}
|
|
119014
120738
|
let text2;
|
|
119015
120739
|
try {
|
|
119016
|
-
text2 =
|
|
120740
|
+
text2 = readFileSync16(file2, "utf8");
|
|
119017
120741
|
} catch (err) {
|
|
119018
120742
|
if (depth > 0 && err.code !== "ENOENT") notes.push(`${tildePath(file2, opts.home)} could not be read`);
|
|
119019
120743
|
return;
|
|
@@ -119038,11 +120762,11 @@ function readSshConfig(opts) {
|
|
|
119038
120762
|
}
|
|
119039
120763
|
for (const arg of split2.args) {
|
|
119040
120764
|
let path2 = arg;
|
|
119041
|
-
if (path2.startsWith("~/")) path2 =
|
|
120765
|
+
if (path2.startsWith("~/")) path2 = join33(opts.home, path2.slice(2));
|
|
119042
120766
|
else if (path2.startsWith("~")) {
|
|
119043
120767
|
notes.push(`An Include of another user's folder (${where(file2, n + 1)}) was not read`);
|
|
119044
120768
|
continue;
|
|
119045
|
-
} else if (!isAbsolute10(path2)) path2 =
|
|
120769
|
+
} else if (!isAbsolute10(path2)) path2 = join33(opts.home, ".ssh", path2);
|
|
119046
120770
|
for (const f of expandGlob(path2)) read(f, depth + 1);
|
|
119047
120771
|
}
|
|
119048
120772
|
}
|
|
@@ -119077,7 +120801,7 @@ function resolveAlias(alias, opts = {}) {
|
|
|
119077
120801
|
resolve12(r);
|
|
119078
120802
|
};
|
|
119079
120803
|
const none = (error51) => ({ hostName: null, user: null, port: null, error: error51 });
|
|
119080
|
-
const child =
|
|
120804
|
+
const child = spawn8(opts.ssh ?? "ssh", args, { env: opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
119081
120805
|
const timer = setTimeout(() => {
|
|
119082
120806
|
child.kill("SIGKILL");
|
|
119083
120807
|
finish2(none("ssh -G did not answer"));
|
|
@@ -119092,7 +120816,7 @@ var MAX_HOSTS = 100;
|
|
|
119092
120816
|
var RESOLVE_AT_ONCE = 8;
|
|
119093
120817
|
async function listSshHosts(opts = {}) {
|
|
119094
120818
|
const home = opts.home ?? homedir13();
|
|
119095
|
-
const file2 = opts.configFile ??
|
|
120819
|
+
const file2 = opts.configFile ?? join33(home, ".ssh", "config");
|
|
119096
120820
|
const read = readSshConfig({ file: file2, home });
|
|
119097
120821
|
const notes = [...read.notes];
|
|
119098
120822
|
const listed = read.hosts.slice(0, MAX_HOSTS);
|
|
@@ -119455,7 +121179,10 @@ var Qualifier = class {
|
|
|
119455
121179
|
const sid = typeof e.sessionId === "string" ? this.id(e.sessionId) : void 0;
|
|
119456
121180
|
const base2 = { ...e, ...sid !== void 0 ? { sessionId: sid } : {} };
|
|
119457
121181
|
switch (e.type) {
|
|
121182
|
+
// Another machine's agent asking its own windows to apply (#665) must never relaunch the hub's window
|
|
119458
121183
|
case "update_status":
|
|
121184
|
+
case "update_apply_request":
|
|
121185
|
+
case "update_apply_go":
|
|
119459
121186
|
case "themes_changed":
|
|
119460
121187
|
case "app_state_changed":
|
|
119461
121188
|
case "agent_versions":
|
|
@@ -120480,7 +122207,7 @@ function linkedSessions(machines, route = notRouted, hubName2) {
|
|
|
120480
122207
|
async updateSettings(sessionId, s) {
|
|
120481
122208
|
const m = owner(sessionId);
|
|
120482
122209
|
if (!m) return { ok: false, error: `Not a session of a linked machine: ${sessionId}` };
|
|
120483
|
-
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" }) }));
|
|
120484
122211
|
},
|
|
120485
122212
|
async findProjects(ref) {
|
|
120486
122213
|
const wanted = ref.trim();
|
|
@@ -120875,6 +122602,9 @@ var ROUTES = {
|
|
|
120875
122602
|
"updates.setAuto": HUB,
|
|
120876
122603
|
"updates.setAutoApply": HUB,
|
|
120877
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,
|
|
120878
122608
|
"machines.list": HUB,
|
|
120879
122609
|
"machines.add": HUB,
|
|
120880
122610
|
"machines.sshHosts": HUB,
|
|
@@ -121198,7 +122928,7 @@ function storeMirror(store2) {
|
|
|
121198
122928
|
var start = new StartSequence();
|
|
121199
122929
|
start.at("launch variables");
|
|
121200
122930
|
var launch = takeLaunchEnv();
|
|
121201
|
-
var BUILD = true ? "
|
|
122931
|
+
var BUILD = true ? "29ffab5" : "dev";
|
|
121202
122932
|
var { values } = parseArgs({
|
|
121203
122933
|
options: {
|
|
121204
122934
|
port: { type: "string", default: "5175" },
|
|
@@ -121219,17 +122949,17 @@ start.at("data folder");
|
|
|
121219
122949
|
var movedNote = null;
|
|
121220
122950
|
function defaultDbPath() {
|
|
121221
122951
|
const isDev = process.env.CC_DEV === "1";
|
|
121222
|
-
const dir =
|
|
121223
|
-
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);
|
|
121224
122954
|
movedNote = migrateLegacyDataDir(legacy, dir) ? `[agent-host] data folder moved: ${legacy} -> ${dir}` : null;
|
|
121225
122955
|
mkdirSync11(dir, { recursive: true });
|
|
121226
122956
|
process.env.CC_DATA_DIR = dir;
|
|
121227
|
-
return
|
|
122957
|
+
return join34(dir, "store.db");
|
|
121228
122958
|
}
|
|
121229
122959
|
var dbPath = values.memory ? ":memory:" : values.db ?? defaultDbPath();
|
|
121230
122960
|
start.at("log");
|
|
121231
122961
|
var stopLog = dbPath === ":memory:" ? () => {
|
|
121232
|
-
} : teeStderrToFile(hostLogPath(
|
|
122962
|
+
} : teeStderrToFile(hostLogPath(dirname12(dbPath)));
|
|
121233
122963
|
if (dbPath !== ":memory:") console.error(startupBanner({ build: BUILD, db: dbPath, pid: process.pid }));
|
|
121234
122964
|
if (movedNote) console.error(movedNote);
|
|
121235
122965
|
start.at("path");
|
|
@@ -121285,7 +123015,7 @@ try {
|
|
|
121285
123015
|
}
|
|
121286
123016
|
var adapters = createAdapters();
|
|
121287
123017
|
start.at("keeper children");
|
|
121288
|
-
var held2 = launch.underKeeper ? await connectHeldChildren(
|
|
123018
|
+
var held2 = launch.underKeeper ? await connectHeldChildren(dirname12(dbPath)) : null;
|
|
121289
123019
|
start.at("services");
|
|
121290
123020
|
var screens = new Gate();
|
|
121291
123021
|
var events = new Gate();
|
|
@@ -121302,7 +123032,7 @@ var mgr = new SessionManager(
|
|
|
121302
123032
|
},
|
|
121303
123033
|
(sessionId) => bridgeAddress(launch.frontDoor, port, token, sessionId),
|
|
121304
123034
|
// Worktrees are created next to the data folder — dev and the packaged app never touch each other's worktrees
|
|
121305
|
-
|
|
123035
|
+
join34(dirname12(dbPath), "worktrees"),
|
|
121306
123036
|
held2 ? { processes: held2.processes, keptSessions: held2.kept.sessionIds } : {}
|
|
121307
123037
|
);
|
|
121308
123038
|
mgr.useProjectsHome(() => projectsHome());
|
|
@@ -121336,7 +123066,7 @@ if (held2) {
|
|
|
121336
123066
|
}
|
|
121337
123067
|
var AUTO_UPDATE_CHECK_KEY = "updates.auto";
|
|
121338
123068
|
var AUTO_APPLY_UPDATES_KEY = "updates.autoApply";
|
|
121339
|
-
var themes = new ThemeFiles(
|
|
123069
|
+
var themes = new ThemeFiles(join34(dataRoot(), "themes"), () => events.push({ type: "themes_changed" }));
|
|
121340
123070
|
await themes.start().catch((e) => console.error(`[themes] the themes folder is unavailable: ${e.message}`));
|
|
121341
123071
|
var updates = new UpdateService((status) => events.push({ type: "update_status", status }), {
|
|
121342
123072
|
// On by default when nothing has been saved yet. Since the check is read-only and every failure
|
|
@@ -121350,6 +123080,8 @@ var updates = new UpdateService((status) => events.push({ type: "update_status",
|
|
|
121350
123080
|
writeAutoApply: (enabled) => store.setAppSetting(AUTO_APPLY_UPDATES_KEY, String(enabled))
|
|
121351
123081
|
});
|
|
121352
123082
|
mgr.useUpdates(updates);
|
|
123083
|
+
var updateApply = new UpdateApplyBroker({ push: (e) => events.push(e), windows: () => server.clientCount });
|
|
123084
|
+
mgr.useUpdateApply(updateApply);
|
|
121353
123085
|
var AUTO_APPLY_AGENT_VERSIONS_KEY = "agents.autoApplyVersions";
|
|
121354
123086
|
var AGENT_VERSIONS_SEEN_KEY = "agents.versionsSeen";
|
|
121355
123087
|
var agentVersions = new AgentVersionService({
|
|
@@ -121457,7 +123189,8 @@ var router = new Router({
|
|
|
121457
123189
|
heldPids: held2?.heldPids,
|
|
121458
123190
|
machines: links,
|
|
121459
123191
|
stopHost: launch.startedByServe && !launch.underKeeper ? () => stopFromRequest() : void 0,
|
|
121460
|
-
activity: () => activityCounts(activity())
|
|
123192
|
+
activity: () => activityCounts(activity()),
|
|
123193
|
+
updateApply
|
|
121461
123194
|
}),
|
|
121462
123195
|
machines: () => links.all()
|
|
121463
123196
|
});
|
|
@@ -121468,7 +123201,10 @@ var server = new HostServer({
|
|
|
121468
123201
|
allowedOrigins,
|
|
121469
123202
|
onRpc: router.handle,
|
|
121470
123203
|
// A window's connection ended: the pinned views it leased wait for a claim, then close (#392, view-host.ts)
|
|
121471
|
-
onClientGone: (client) =>
|
|
123204
|
+
onClientGone: (client) => {
|
|
123205
|
+
views.holderGone(client);
|
|
123206
|
+
updateApply.clientGone(client);
|
|
123207
|
+
},
|
|
121472
123208
|
// Every HTTP route sits behind this secret (transport/http.ts)
|
|
121473
123209
|
http: { secret: httpSecret, routes: views.routes },
|
|
121474
123210
|
// Which build this is and where it came from, in every hello_ok (#280, keeper-link.ts)
|
|
@@ -121555,7 +123291,7 @@ var endings = wireEndings({
|
|
|
121555
123291
|
heldChildren: held2 !== null,
|
|
121556
123292
|
leave: (mode, handOver) => void shutdown.leave(mode, handOver),
|
|
121557
123293
|
force: (why) => shutdown.force(why),
|
|
121558
|
-
record: crashRecorder(
|
|
123294
|
+
record: crashRecorder(join34(dirname12(dbPath), "host-errors.log"), (line) => console.error(line)),
|
|
121559
123295
|
stdin: process.stdin,
|
|
121560
123296
|
watchParent: values["watch-parent"] === true,
|
|
121561
123297
|
log: (line) => console.error(line)
|