@centralu/host-linux-arm64 0.1.0-beta.19 → 0.1.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/host/bundle-info.json +2 -2
- package/host/main.mjs +1305 -595
- package/package.json +1 -1
package/host/main.mjs
CHANGED
|
@@ -9373,7 +9373,7 @@ function dn(e) {
|
|
|
9373
9373
|
function or(e) {
|
|
9374
9374
|
return e.length > 0 && e.every(dn);
|
|
9375
9375
|
}
|
|
9376
|
-
function
|
|
9376
|
+
function rm4(e) {
|
|
9377
9377
|
return Is(e).some((t) => t.endsWith(".jsonl"));
|
|
9378
9378
|
}
|
|
9379
9379
|
function Is(e) {
|
|
@@ -9523,7 +9523,7 @@ function tS(e, t) {
|
|
|
9523
9523
|
if (U1e(t)) return q(e, M1e);
|
|
9524
9524
|
if (D1e(t)) return q(e, I1e);
|
|
9525
9525
|
if (N1e(t)) return q(e, L1e);
|
|
9526
|
-
return
|
|
9526
|
+
return rm4(t) ? q(e, sm) : void 0;
|
|
9527
9527
|
}
|
|
9528
9528
|
function z1e(e) {
|
|
9529
9529
|
return j1e(`${e}.json`);
|
|
@@ -9532,7 +9532,7 @@ function j1e(e) {
|
|
|
9532
9532
|
return Is(e).includes(F1e);
|
|
9533
9533
|
}
|
|
9534
9534
|
function nS(e) {
|
|
9535
|
-
return Array.isArray(e) && e.some((t) => typeof t === "string" &&
|
|
9535
|
+
return Array.isArray(e) && e.some((t) => typeof t === "string" && rm4(t));
|
|
9536
9536
|
}
|
|
9537
9537
|
function H1e(e) {
|
|
9538
9538
|
return e === "user" || e === "project" || e === "local";
|
|
@@ -11008,7 +11008,7 @@ function L3(e) {
|
|
|
11008
11008
|
}
|
|
11009
11009
|
function U3(e) {
|
|
11010
11010
|
let t = N3(e);
|
|
11011
|
-
if (
|
|
11011
|
+
if (rm4(t)) return;
|
|
11012
11012
|
let n = vt(), r = [t], o = M3(e);
|
|
11013
11013
|
while (o !== n && r.length <= h2e + 1) {
|
|
11014
11014
|
let l = M3(o);
|
|
@@ -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 dirname11, join as
|
|
42963
|
+
import { dirname as dirname11, join as join33 } 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.21";
|
|
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";
|
|
@@ -58032,7 +58032,7 @@ var ApprovalDetail = external_exports.discriminatedUnion("kind", [
|
|
|
58032
58032
|
}),
|
|
58033
58033
|
/**
|
|
58034
58034
|
* One project reaching another (#371) — raised by **the host** in the calling session, the first time a session of
|
|
58035
|
-
* `from` asks to reach `to`. `access` says how: 'delegate'
|
|
58035
|
+
* `from` asks to reach `to`. `access` says how: 'delegate' gives an existing session in `to` a task
|
|
58036
58036
|
* (ask_project, part B); 'apps' attaches one of `to`'s apps to the caller (part A, with `app` naming it).
|
|
58037
58037
|
*
|
|
58038
58038
|
* Unlike `capability`, this one has all three answers: allow once (this call only), always (remembered for this
|
|
@@ -58046,14 +58046,16 @@ var ApprovalDetail = external_exports.discriminatedUnion("kind", [
|
|
|
58046
58046
|
id: external_exports.string(),
|
|
58047
58047
|
name: external_exports.string(),
|
|
58048
58048
|
/**
|
|
58049
|
-
* Whether the person trusts the target project (#382). `false` is said on the card:
|
|
58050
|
-
*
|
|
58049
|
+
* Whether the person trusts the target project (#382). `false` is said on the card: the existing target session
|
|
58050
|
+
* follows that trust. Absent from a host that predates it, read as not saying.
|
|
58051
58051
|
*/
|
|
58052
58052
|
trusted: external_exports.boolean().optional()
|
|
58053
58053
|
}),
|
|
58054
58054
|
/** One line for a person to read: what the caller wants done there (the task, cut short; or the app's use) */
|
|
58055
58055
|
text: external_exports.string(),
|
|
58056
|
-
app: external_exports.object({ appId: external_exports.string(), name: external_exports.string() }).optional()
|
|
58056
|
+
app: external_exports.object({ appId: external_exports.string(), name: external_exports.string() }).optional(),
|
|
58057
|
+
/** The existing session chosen for ask_project; absent from older hosts. */
|
|
58058
|
+
session: external_exports.object({ id: external_exports.string(), name: external_exports.string() }).optional()
|
|
58057
58059
|
}),
|
|
58058
58060
|
/**
|
|
58059
58061
|
* A session asks to start another agent in its own project (#561, start_agent) — raised by **the host** in the
|
|
@@ -58249,6 +58251,8 @@ var AdapterCapabilities = external_exports.object({
|
|
|
58249
58251
|
* The idle check (#297) needs that difference — a session whose background work is invisible is never called idle.
|
|
58250
58252
|
*/
|
|
58251
58253
|
backgroundTasks: external_exports.boolean().default(false),
|
|
58254
|
+
/** Whether send accepts input during a running turn without interrupting it (#592). Absent is unsupported. */
|
|
58255
|
+
midTurnInput: external_exports.boolean().optional(),
|
|
58252
58256
|
/**
|
|
58253
58257
|
* Whether this tool can defer individual Centralu tool definitions until the model asks for
|
|
58254
58258
|
* them. This is a serving property, not a list of tool names: the host uses it when it counts
|
|
@@ -58531,7 +58535,9 @@ var UiPreferences = external_exports.object({
|
|
|
58531
58535
|
* preference, but this is the one record the person's settings already live in; the host reads
|
|
58532
58536
|
* it when a session starts and on each call.
|
|
58533
58537
|
*/
|
|
58534
|
-
sessionTools: external_exports.boolean()
|
|
58538
|
+
sessionTools: external_exports.boolean(),
|
|
58539
|
+
/** 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()
|
|
58535
58541
|
});
|
|
58536
58542
|
var UiPreferencesPatch = UiPreferences.partial();
|
|
58537
58543
|
var DEFAULT_UI_PREFERENCES = {
|
|
@@ -59625,14 +59631,9 @@ var SessionInfo = external_exports.object({
|
|
|
59625
59631
|
*/
|
|
59626
59632
|
parentSessionId: external_exports.string().nullable().default(null),
|
|
59627
59633
|
/**
|
|
59628
|
-
* The session that
|
|
59629
|
-
*
|
|
59630
|
-
*
|
|
59631
|
-
* The delegated session is an ordinary session of its own project, so the person can watch it and step in; this
|
|
59632
|
-
* line is what marks it "asked by" and links back to the caller. On the row (store v45), so the mark survives a
|
|
59633
|
-
* restart, and the caller's next ask reuses the session it already asked. The caller may since have been deleted:
|
|
59634
|
-
* the id then names a session that is no longer listed. Optional rather than defaulted so a frame or a fixture
|
|
59635
|
-
* that predates it needs no change: absent and null read the same.
|
|
59634
|
+
* The same-project session that started this agent (`start_agent`, #561). Person-created and app
|
|
59635
|
+
* sessions have no owner. Old cross-project ask_project ownership is cleared on host startup
|
|
59636
|
+
* (#614); cross-project tasks retain their asking session in conversation `from` instead.
|
|
59636
59637
|
*/
|
|
59637
59638
|
askedBy: external_exports.string().nullable().optional(),
|
|
59638
59639
|
...machineField,
|
|
@@ -59837,6 +59838,14 @@ var MessageImage = external_exports.discriminatedUnion("ok", [
|
|
|
59837
59838
|
external_exports.object({ ok: external_exports.literal(true), mime: external_exports.string(), data: external_exports.string(), file: external_exports.string().optional() }),
|
|
59838
59839
|
external_exports.object({ ok: external_exports.literal(false), reason: MessageImageRefusal, message: external_exports.string(), file: external_exports.string().optional() })
|
|
59839
59840
|
]);
|
|
59841
|
+
var AppHomeViewResult = external_exports.object({
|
|
59842
|
+
instanceId: external_exports.string(),
|
|
59843
|
+
tool: external_exports.string(),
|
|
59844
|
+
resourceUri: external_exports.string(),
|
|
59845
|
+
toolInput: external_exports.record(external_exports.string(), external_exports.unknown()),
|
|
59846
|
+
toolResult: external_exports.looseObject({ content: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())) }),
|
|
59847
|
+
runId: external_exports.string()
|
|
59848
|
+
});
|
|
59840
59849
|
var RpcMethods = {
|
|
59841
59850
|
"agents.createSession": { params: CreateSessionParams, result: SessionInfo },
|
|
59842
59851
|
"agents.send": {
|
|
@@ -60433,7 +60442,8 @@ var RpcMethods = {
|
|
|
60433
60442
|
* tool-result).
|
|
60434
60443
|
*
|
|
60435
60444
|
* If there is no `home`, or that tool did not declare a screen, or the app cannot be reached
|
|
60436
|
-
* (not trusted, or failed to start), this fails with a reason and leaves no
|
|
60445
|
+
* (not trusted, or failed to start), this fails with a reason and leaves no live screen behind. A
|
|
60446
|
+
* keyed open keeps only a failure reservation during the lease grace period, without holding the app. If
|
|
60437
60447
|
* the app itself answered with a failure, this still opens — drawing a failure is also the
|
|
60438
60448
|
* screen's job.
|
|
60439
60449
|
*
|
|
@@ -60442,21 +60452,16 @@ var RpcMethods = {
|
|
|
60442
60452
|
* machine. Absent: this host's own app, as before.
|
|
60443
60453
|
*/
|
|
60444
60454
|
"apps.openView": {
|
|
60455
|
+
// openKey: one client-chosen UUID per logical open. Repeats reuse the instance and home outcome,
|
|
60456
|
+
// including after a planned host handover, so a lost answer never runs the home tool twice.
|
|
60445
60457
|
/*
|
|
60446
60458
|
* `lease` (#392, additive): the view belongs to this window's connection. Once the connection ends, the host waits
|
|
60447
60459
|
* a few minutes for a window to claim it (`apps.holdViews`) and then closes it, so a reloaded or closed window does
|
|
60448
60460
|
* not hold its apps open for good. Without it (an older window, or a linked machine's view) the view lives until
|
|
60449
60461
|
* `apps.closeView`.
|
|
60450
60462
|
*/
|
|
60451
|
-
params: external_exports.object({ appId: AppId, projectId: ProjectId.nullable(), ...machineParam, lease: external_exports.boolean().optional() }),
|
|
60452
|
-
result:
|
|
60453
|
-
instanceId: external_exports.string(),
|
|
60454
|
-
tool: external_exports.string(),
|
|
60455
|
-
resourceUri: external_exports.string(),
|
|
60456
|
-
toolInput: external_exports.record(external_exports.string(), external_exports.unknown()),
|
|
60457
|
-
toolResult: external_exports.looseObject({ content: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())) }),
|
|
60458
|
-
runId: external_exports.string()
|
|
60459
|
-
})
|
|
60463
|
+
params: external_exports.object({ appId: AppId, projectId: ProjectId.nullable(), ...machineParam, lease: external_exports.boolean().optional(), openKey: external_exports.string().uuid().optional() }),
|
|
60464
|
+
result: AppHomeViewResult
|
|
60460
60465
|
},
|
|
60461
60466
|
/**
|
|
60462
60467
|
* Closes the pinned screen (M4 B-2). Releases the app the instance was holding on to — an app
|
|
@@ -60467,6 +60472,11 @@ var RpcMethods = {
|
|
|
60467
60472
|
params: external_exports.object({ instanceId: external_exports.string() }),
|
|
60468
60473
|
result: external_exports.object({ ok: external_exports.literal(true) })
|
|
60469
60474
|
},
|
|
60475
|
+
/** Reclaims a keyed home open after losing its answer; never calls the home tool. */
|
|
60476
|
+
"apps.recoverView": {
|
|
60477
|
+
params: external_exports.object({ appId: AppId, projectId: ProjectId.nullable(), ...machineParam, lease: external_exports.boolean().optional(), openKey: external_exports.string().uuid() }),
|
|
60478
|
+
result: AppHomeViewResult
|
|
60479
|
+
},
|
|
60470
60480
|
/**
|
|
60471
60481
|
* A window claims the pinned screens it shows (#392), after every reconnect: each leased instance
|
|
60472
60482
|
* named becomes this connection's again and stops waiting to be closed. `missing` lists the ids
|
|
@@ -61749,12 +61759,12 @@ var KeeperAgentProcess = class _KeeperAgentProcess extends EventEmitter {
|
|
|
61749
61759
|
await Promise.race([this.ready, new Promise((r) => setTimeout(r, DETACH_WAIT_MS))]);
|
|
61750
61760
|
const out = this.out;
|
|
61751
61761
|
if (!out) return;
|
|
61752
|
-
await new Promise((
|
|
61753
|
-
if (out.readableEnded || out.destroyed) return
|
|
61754
|
-
const t = setTimeout(
|
|
61762
|
+
await new Promise((resolve12) => {
|
|
61763
|
+
if (out.readableEnded || out.destroyed) return resolve12();
|
|
61764
|
+
const t = setTimeout(resolve12, DETACH_WAIT_MS);
|
|
61755
61765
|
const done = () => {
|
|
61756
61766
|
clearTimeout(t);
|
|
61757
|
-
|
|
61767
|
+
resolve12();
|
|
61758
61768
|
};
|
|
61759
61769
|
out.once("end", done);
|
|
61760
61770
|
out.once("close", done);
|
|
@@ -61771,7 +61781,7 @@ import { createConnection } from "node:net";
|
|
|
61771
61781
|
var CHILDREN_PROTOCOL = 1;
|
|
61772
61782
|
var REQUEST_TIMEOUT_MS = 15e3;
|
|
61773
61783
|
function readAnswer(sock, timeoutMs) {
|
|
61774
|
-
return new Promise((
|
|
61784
|
+
return new Promise((resolve12, reject) => {
|
|
61775
61785
|
let buf = Buffer.alloc(0);
|
|
61776
61786
|
const done = (err, v) => {
|
|
61777
61787
|
clearTimeout(timer);
|
|
@@ -61779,7 +61789,7 @@ function readAnswer(sock, timeoutMs) {
|
|
|
61779
61789
|
sock.off("error", onError);
|
|
61780
61790
|
sock.off("close", onClose);
|
|
61781
61791
|
if (err) reject(err);
|
|
61782
|
-
else
|
|
61792
|
+
else resolve12(v);
|
|
61783
61793
|
};
|
|
61784
61794
|
const onData = (d2) => {
|
|
61785
61795
|
buf = Buffer.concat([buf, d2]);
|
|
@@ -61808,7 +61818,7 @@ function readAnswer(sock, timeoutMs) {
|
|
|
61808
61818
|
});
|
|
61809
61819
|
}
|
|
61810
61820
|
function connect(path2, first, timeoutMs) {
|
|
61811
|
-
return new Promise((
|
|
61821
|
+
return new Promise((resolve12, reject) => {
|
|
61812
61822
|
const sock = createConnection(path2);
|
|
61813
61823
|
const onError = (e) => reject(e);
|
|
61814
61824
|
sock.once("error", onError);
|
|
@@ -61822,7 +61832,7 @@ function connect(path2, first, timeoutMs) {
|
|
|
61822
61832
|
sock.destroy();
|
|
61823
61833
|
return reject(new Error(String(answer.error ?? "the keeper refused")));
|
|
61824
61834
|
}
|
|
61825
|
-
|
|
61835
|
+
resolve12({ sock, answer });
|
|
61826
61836
|
},
|
|
61827
61837
|
(e) => {
|
|
61828
61838
|
sock.destroy();
|
|
@@ -61908,12 +61918,12 @@ var KeeperChildren = class _KeeperChildren extends EventEmitter2 {
|
|
|
61908
61918
|
request(op, body = {}) {
|
|
61909
61919
|
if (this.closed) return Promise.reject(new Error("the connection to the keeper was lost"));
|
|
61910
61920
|
const rid = this.nextRid++;
|
|
61911
|
-
return new Promise((
|
|
61921
|
+
return new Promise((resolve12, reject) => {
|
|
61912
61922
|
const timer = setTimeout(() => {
|
|
61913
61923
|
if (this.pending.delete(rid)) reject(new Error(`the keeper did not answer ${op}`));
|
|
61914
61924
|
}, REQUEST_TIMEOUT_MS);
|
|
61915
61925
|
timer.unref();
|
|
61916
|
-
this.pending.set(rid, { resolve:
|
|
61926
|
+
this.pending.set(rid, { resolve: resolve12, reject, timer });
|
|
61917
61927
|
this.sock.write(`${JSON.stringify({ ...body, op, rid })}
|
|
61918
61928
|
`);
|
|
61919
61929
|
});
|
|
@@ -62094,12 +62104,12 @@ var KeeperPty = class _KeeperPty {
|
|
|
62094
62104
|
await Promise.race([this.ready, new Promise((r) => setTimeout(r, 1e3))]);
|
|
62095
62105
|
const sock = this.sock;
|
|
62096
62106
|
if (!sock) return;
|
|
62097
|
-
await new Promise((
|
|
62098
|
-
if (sock.readableEnded || sock.destroyed) return
|
|
62099
|
-
const t = setTimeout(
|
|
62107
|
+
await new Promise((resolve12) => {
|
|
62108
|
+
if (sock.readableEnded || sock.destroyed) return resolve12();
|
|
62109
|
+
const t = setTimeout(resolve12, 1e3);
|
|
62100
62110
|
const done = () => {
|
|
62101
62111
|
clearTimeout(t);
|
|
62102
|
-
|
|
62112
|
+
resolve12();
|
|
62103
62113
|
};
|
|
62104
62114
|
sock.once("end", done);
|
|
62105
62115
|
sock.once("close", done);
|
|
@@ -62462,7 +62472,7 @@ var Drain = class {
|
|
|
62462
62472
|
*/
|
|
62463
62473
|
track(label, run) {
|
|
62464
62474
|
if (this.refusing) return Promise.reject(new DrainCut(REFUSED_MESSAGE));
|
|
62465
|
-
return new Promise((
|
|
62475
|
+
return new Promise((resolve12, reject) => {
|
|
62466
62476
|
let settled = false;
|
|
62467
62477
|
const settle = () => {
|
|
62468
62478
|
if (settled) return false;
|
|
@@ -62474,7 +62484,7 @@ var Drain = class {
|
|
|
62474
62484
|
const entry = { label, cut: (e) => settle() && reject(e) };
|
|
62475
62485
|
this.inflight.add(entry);
|
|
62476
62486
|
Promise.resolve().then(run).then(
|
|
62477
|
-
(v) => settle() &&
|
|
62487
|
+
(v) => settle() && resolve12(v),
|
|
62478
62488
|
(e) => settle() && reject(e)
|
|
62479
62489
|
);
|
|
62480
62490
|
});
|
|
@@ -62623,7 +62633,7 @@ var HostServer = class _HostServer {
|
|
|
62623
62633
|
return this.allowedOrigins.has(origin);
|
|
62624
62634
|
}
|
|
62625
62635
|
listen() {
|
|
62626
|
-
return new Promise((
|
|
62636
|
+
return new Promise((resolve12, reject) => {
|
|
62627
62637
|
const onError = (err) => {
|
|
62628
62638
|
this.listenError = null;
|
|
62629
62639
|
if (err.code === "EADDRINUSE") {
|
|
@@ -62640,7 +62650,7 @@ var HostServer = class _HostServer {
|
|
|
62640
62650
|
this.listenError = null;
|
|
62641
62651
|
this.http.removeListener("error", onError);
|
|
62642
62652
|
const addr = this.http.address();
|
|
62643
|
-
|
|
62653
|
+
resolve12(typeof addr === "object" && addr ? addr.port : this.opts.port);
|
|
62644
62654
|
});
|
|
62645
62655
|
});
|
|
62646
62656
|
}
|
|
@@ -63017,15 +63027,15 @@ function buildViewCsp(csp) {
|
|
|
63017
63027
|
const frame = sanitizeDomains(csp?.frameDomains);
|
|
63018
63028
|
const base2 = sanitizeDomains(csp?.baseUriDomains);
|
|
63019
63029
|
const r = resource.kept.join(" ");
|
|
63020
|
-
const
|
|
63030
|
+
const join34 = (...parts) => parts.filter(Boolean).join(" ");
|
|
63021
63031
|
const policy = [
|
|
63022
63032
|
"default-src 'none'",
|
|
63023
|
-
`script-src ${
|
|
63024
|
-
`style-src ${
|
|
63025
|
-
`img-src ${
|
|
63026
|
-
`font-src ${
|
|
63027
|
-
`media-src ${
|
|
63028
|
-
`worker-src ${
|
|
63033
|
+
`script-src ${join34("'unsafe-inline'", r)}`,
|
|
63034
|
+
`style-src ${join34("'unsafe-inline'", r)}`,
|
|
63035
|
+
`img-src ${join34("data: blob:", r)}`,
|
|
63036
|
+
`font-src ${join34("data:", r)}`,
|
|
63037
|
+
`media-src ${join34("data: blob:", r)}`,
|
|
63038
|
+
`worker-src ${join34("blob:", r)}`,
|
|
63029
63039
|
`connect-src ${connect2.kept.length ? connect2.kept.join(" ") : "'none'"}`,
|
|
63030
63040
|
`frame-src ${frame.kept.length ? frame.kept.join(" ") : "'none'"}`,
|
|
63031
63041
|
"form-action 'none'",
|
|
@@ -63175,6 +63185,7 @@ var ViewHost = class _ViewHost {
|
|
|
63175
63185
|
}
|
|
63176
63186
|
opts;
|
|
63177
63187
|
instances = /* @__PURE__ */ new Map();
|
|
63188
|
+
openingHomes = /* @__PURE__ */ new Map();
|
|
63178
63189
|
origins = /* @__PURE__ */ new Map();
|
|
63179
63190
|
/** Origin servers being closed, by key: one started again for the same key waits for its port to be free */
|
|
63180
63191
|
closingOrigins = /* @__PURE__ */ new Map();
|
|
@@ -63212,6 +63223,42 @@ var ViewHost = class _ViewHost {
|
|
|
63212
63223
|
this.instances.set(id2, { id: id2, app: ref, uri, doc: null, release, lease });
|
|
63213
63224
|
return { instanceId: id2 };
|
|
63214
63225
|
}
|
|
63226
|
+
/** A logical home open may run its tool once, even when its answer is lost in a swap. */
|
|
63227
|
+
homeOpen(app2, key, holder) {
|
|
63228
|
+
const inst = [...this.instances.values()].find((i) => i.home?.key === key);
|
|
63229
|
+
if (!inst) return null;
|
|
63230
|
+
if (!sameApp(inst.app, app2)) fail("This open key belongs to another app");
|
|
63231
|
+
if (inst.home.outcome) {
|
|
63232
|
+
if (holder !== void 0) this.hold([inst.id], holder);
|
|
63233
|
+
return Promise.resolve(inst.home.outcome);
|
|
63234
|
+
}
|
|
63235
|
+
if (inst.home.error) return Promise.reject(new Error(inst.home.error));
|
|
63236
|
+
const opening = this.openingHomes.get(inst.id);
|
|
63237
|
+
if (opening) {
|
|
63238
|
+
if (holder !== void 0) this.hold([inst.id], holder);
|
|
63239
|
+
return opening;
|
|
63240
|
+
}
|
|
63241
|
+
return Promise.reject(new Error(
|
|
63242
|
+
"Host restarted while the home tool was running \u2014 the app may or may not have done this; check before opening again"
|
|
63243
|
+
));
|
|
63244
|
+
}
|
|
63245
|
+
rememberHome(instanceId, key, work) {
|
|
63246
|
+
const inst = this.instances.get(instanceId);
|
|
63247
|
+
inst.home = { key };
|
|
63248
|
+
const opening = work().then((outcome) => {
|
|
63249
|
+
inst.home.outcome = outcome;
|
|
63250
|
+
return outcome;
|
|
63251
|
+
}, (e) => {
|
|
63252
|
+
inst.home.error = e instanceof Error ? e.message : String(e);
|
|
63253
|
+
inst.release?.();
|
|
63254
|
+
inst.release = null;
|
|
63255
|
+
inst.lease = { holder: null, orphanSince: Date.now() };
|
|
63256
|
+
this.scheduleSweep();
|
|
63257
|
+
throw e;
|
|
63258
|
+
}).finally(() => this.openingHomes.delete(instanceId));
|
|
63259
|
+
this.openingHomes.set(instanceId, opening);
|
|
63260
|
+
return opening;
|
|
63261
|
+
}
|
|
63215
63262
|
close(instanceId) {
|
|
63216
63263
|
this.drop(instanceId);
|
|
63217
63264
|
}
|
|
@@ -63265,7 +63312,7 @@ var ViewHost = class _ViewHost {
|
|
|
63265
63312
|
}
|
|
63266
63313
|
/** Every open instance, oldest first — what a planned hand-over records (view-handover.ts) */
|
|
63267
63314
|
list() {
|
|
63268
|
-
return [...this.instances.values()].map((i) => ({ id: i.id, app: { ...i.app }, uri: i.uri, ...i.lease ? { leased: true } : {} }));
|
|
63315
|
+
return [...this.instances.values()].map((i) => ({ id: i.id, app: { ...i.app }, uri: i.uri, ...i.lease ? { leased: true } : {}, ...i.home ? { home: i.home } : {} }));
|
|
63269
63316
|
}
|
|
63270
63317
|
/**
|
|
63271
63318
|
* Opens again, **under their old ids**, the instances a previous host handed over (#280: a build
|
|
@@ -63292,8 +63339,22 @@ var ViewHost = class _ViewHost {
|
|
|
63292
63339
|
this.log(`[agent-host] view ${v.uri} (${_ViewHost.originKey(ref)}) not restored: ${err.message}`);
|
|
63293
63340
|
continue;
|
|
63294
63341
|
}
|
|
63295
|
-
|
|
63296
|
-
|
|
63342
|
+
let lease = v.leased === true ? { holder: null, orphanSince: now } : null;
|
|
63343
|
+
let home;
|
|
63344
|
+
if (v.home && typeof v.home.key === "string" && RpcMethods["apps.openView"].params.shape.openKey.safeParse(v.home.key).success) {
|
|
63345
|
+
const outcome = RpcMethods["apps.openView"].result.safeParse(v.home.outcome);
|
|
63346
|
+
home = {
|
|
63347
|
+
key: v.home.key,
|
|
63348
|
+
...outcome.success && outcome.data.instanceId === v.id ? { outcome: outcome.data } : {},
|
|
63349
|
+
...typeof v.home.error === "string" ? { error: v.home.error } : {}
|
|
63350
|
+
};
|
|
63351
|
+
}
|
|
63352
|
+
if (home && !home.outcome) {
|
|
63353
|
+
release?.();
|
|
63354
|
+
release = null;
|
|
63355
|
+
lease = { holder: null, orphanSince: now };
|
|
63356
|
+
}
|
|
63357
|
+
this.instances.set(v.id, { id: v.id, app: ref, uri: v.uri, doc: null, release, lease, ...home ? { home } : {} });
|
|
63297
63358
|
opened.push(v.id);
|
|
63298
63359
|
}
|
|
63299
63360
|
if (opened.some((id2) => this.instances.get(id2)?.lease)) this.scheduleSweep();
|
|
@@ -68676,14 +68737,14 @@ function inputRequiredRoundsExceededMessage(method, maxRounds) {
|
|
|
68676
68737
|
return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
|
|
68677
68738
|
}
|
|
68678
68739
|
function sleep(ms3, signal) {
|
|
68679
|
-
return new Promise((
|
|
68740
|
+
return new Promise((resolve12, reject) => {
|
|
68680
68741
|
if (signal?.aborted) {
|
|
68681
68742
|
reject(signal.reason instanceof SdkError ? signal.reason : new SdkError(SdkErrorCode.RequestTimeout, String(signal.reason)));
|
|
68682
68743
|
return;
|
|
68683
68744
|
}
|
|
68684
68745
|
const timer = setTimeout(() => {
|
|
68685
68746
|
signal?.removeEventListener("abort", onAbort);
|
|
68686
|
-
|
|
68747
|
+
resolve12();
|
|
68687
68748
|
}, ms3);
|
|
68688
68749
|
const onAbort = () => {
|
|
68689
68750
|
clearTimeout(timer);
|
|
@@ -69522,7 +69583,7 @@ var Protocol = class {
|
|
|
69522
69583
|
const flowStartedAt = Date.now();
|
|
69523
69584
|
let onAbort;
|
|
69524
69585
|
let cleanupMessageId;
|
|
69525
|
-
return new Promise((
|
|
69586
|
+
return new Promise((resolve12, reject) => {
|
|
69526
69587
|
const earlyReject = (error51) => {
|
|
69527
69588
|
reject(error51);
|
|
69528
69589
|
};
|
|
@@ -69591,7 +69652,7 @@ var Protocol = class {
|
|
|
69591
69652
|
}
|
|
69592
69653
|
if (decoded.kind === "invalid") return reject(decoded.error);
|
|
69593
69654
|
if (decoded.kind === "input_required") {
|
|
69594
|
-
if (options?.allowInputRequired === true) return
|
|
69655
|
+
if (options?.allowInputRequired === true) return resolve12(manualInputRequiredValue(decoded));
|
|
69595
69656
|
const flow = {
|
|
69596
69657
|
codec: codec2,
|
|
69597
69658
|
request,
|
|
@@ -69603,11 +69664,11 @@ var Protocol = class {
|
|
|
69603
69664
|
params
|
|
69604
69665
|
}, resultSchema, legOptions)
|
|
69605
69666
|
};
|
|
69606
|
-
return
|
|
69667
|
+
return resolve12(this._resolveNonCompleteResult(decoded, flow));
|
|
69607
69668
|
}
|
|
69608
69669
|
const result = decoded.result;
|
|
69609
69670
|
validateStandardSchema(resultSchema, result).then((parseResult) => {
|
|
69610
|
-
if (parseResult.success)
|
|
69671
|
+
if (parseResult.success) resolve12(parseResult.data);
|
|
69611
69672
|
else reject(new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
|
|
69612
69673
|
}, reject);
|
|
69613
69674
|
});
|
|
@@ -72530,7 +72591,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
72530
72591
|
ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
|
|
72531
72592
|
const schOrFunc = root3.refs[ref];
|
|
72532
72593
|
if (schOrFunc) return schOrFunc;
|
|
72533
|
-
let _sch =
|
|
72594
|
+
let _sch = resolve12.call(this, root3, ref);
|
|
72534
72595
|
if (_sch === void 0) {
|
|
72535
72596
|
const schema = (_a4 = root3.localRefs) === null || _a4 === void 0 ? void 0 : _a4[ref];
|
|
72536
72597
|
const { schemaId } = this.opts;
|
|
@@ -72556,7 +72617,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
72556
72617
|
function sameSchemaEnv(s12, s22) {
|
|
72557
72618
|
return s12.schema === s22.schema && s12.root === s22.root && s12.baseId === s22.baseId;
|
|
72558
72619
|
}
|
|
72559
|
-
function
|
|
72620
|
+
function resolve12(root3, ref) {
|
|
72560
72621
|
let sch;
|
|
72561
72622
|
while (typeof (sch = this.refs[ref]) == "string") ref = sch;
|
|
72562
72623
|
return sch || this.schemas[ref] || resolveSchema.call(this, root3, ref);
|
|
@@ -73006,7 +73067,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
73006
73067
|
else if (typeof uri === "object") uri = parse7(serialize(uri, options), options);
|
|
73007
73068
|
return uri;
|
|
73008
73069
|
}
|
|
73009
|
-
function
|
|
73070
|
+
function resolve12(baseURI, relativeURI, options) {
|
|
73010
73071
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
73011
73072
|
const resolved = resolveComponent(parse7(baseURI, schemelessOptions), parse7(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
73012
73073
|
schemelessOptions.skipEscape = true;
|
|
@@ -73180,7 +73241,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
73180
73241
|
const fastUri = {
|
|
73181
73242
|
SCHEMES,
|
|
73182
73243
|
normalize: normalize2,
|
|
73183
|
-
resolve:
|
|
73244
|
+
resolve: resolve12,
|
|
73184
73245
|
resolveComponent,
|
|
73185
73246
|
equal,
|
|
73186
73247
|
serialize,
|
|
@@ -77795,14 +77856,14 @@ var ProbeWindow = class ProbeWindow2 {
|
|
|
77795
77856
|
*/
|
|
77796
77857
|
async exchange(buildRequest, timeoutMs) {
|
|
77797
77858
|
const id2 = `server-discover-probe-${++this._probeCounter}`;
|
|
77798
|
-
return new Promise((
|
|
77859
|
+
return new Promise((resolve12) => {
|
|
77799
77860
|
let settled = false;
|
|
77800
77861
|
const settle = (reply) => {
|
|
77801
77862
|
if (settled) return;
|
|
77802
77863
|
settled = true;
|
|
77803
77864
|
clearTimeout(timer);
|
|
77804
77865
|
if (this._pending?.id === id2) this._pending = void 0;
|
|
77805
|
-
|
|
77866
|
+
resolve12(reply);
|
|
77806
77867
|
};
|
|
77807
77868
|
const timer = setTimeout(() => settle({ kind: "timeout" }), timeoutMs);
|
|
77808
77869
|
this._pending = {
|
|
@@ -79008,13 +79069,13 @@ var Client = class extends Protocol {
|
|
|
79008
79069
|
let onCallerAbort;
|
|
79009
79070
|
let resolveOpening;
|
|
79010
79071
|
let rejectOpening;
|
|
79011
|
-
const opening = new Promise((
|
|
79012
|
-
resolveOpening =
|
|
79072
|
+
const opening = new Promise((resolve12, reject) => {
|
|
79073
|
+
resolveOpening = resolve12;
|
|
79013
79074
|
rejectOpening = reject;
|
|
79014
79075
|
});
|
|
79015
79076
|
let resolveClosed;
|
|
79016
|
-
const closed = new Promise((
|
|
79017
|
-
resolveClosed =
|
|
79077
|
+
const closed = new Promise((resolve12) => {
|
|
79078
|
+
resolveClosed = resolve12;
|
|
79018
79079
|
});
|
|
79019
79080
|
const settle = (outcome) => {
|
|
79020
79081
|
if (state === "closed") return;
|
|
@@ -79786,8 +79847,8 @@ var StreamTransport = class {
|
|
|
79786
79847
|
}
|
|
79787
79848
|
send(message) {
|
|
79788
79849
|
if (this.closed) return Promise.reject(new Error("app connection closed"));
|
|
79789
|
-
return new Promise((
|
|
79790
|
-
this.writable.write(serializeMessage(message), (err) => err ? reject(err) :
|
|
79850
|
+
return new Promise((resolve12, reject) => {
|
|
79851
|
+
this.writable.write(serializeMessage(message), (err) => err ? reject(err) : resolve12());
|
|
79791
79852
|
});
|
|
79792
79853
|
}
|
|
79793
79854
|
/** Closes only our own state. Closing the pipes is the shutdown rule's job (AppProcess.stop). */
|
|
@@ -79999,12 +80060,12 @@ ${tail}` : head;
|
|
|
79999
80060
|
}
|
|
80000
80061
|
waitExit(ms3) {
|
|
80001
80062
|
if (!this.alive) return Promise.resolve(true);
|
|
80002
|
-
return new Promise((
|
|
80003
|
-
const t = setTimeout(() =>
|
|
80063
|
+
return new Promise((resolve12) => {
|
|
80064
|
+
const t = setTimeout(() => resolve12(false), ms3);
|
|
80004
80065
|
t.unref();
|
|
80005
80066
|
this.exitWaiters.push(() => {
|
|
80006
80067
|
clearTimeout(t);
|
|
80007
|
-
|
|
80068
|
+
resolve12(true);
|
|
80008
80069
|
});
|
|
80009
80070
|
});
|
|
80010
80071
|
}
|
|
@@ -83591,14 +83652,14 @@ function inputRequiredRoundsExceededMessage2(method, maxRounds) {
|
|
|
83591
83652
|
return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
|
|
83592
83653
|
}
|
|
83593
83654
|
function sleep2(ms3, signal) {
|
|
83594
|
-
return new Promise((
|
|
83655
|
+
return new Promise((resolve12, reject) => {
|
|
83595
83656
|
if (signal?.aborted) {
|
|
83596
83657
|
reject(signal.reason instanceof SdkError2 ? signal.reason : new SdkError2(SdkErrorCode2.RequestTimeout, String(signal.reason)));
|
|
83597
83658
|
return;
|
|
83598
83659
|
}
|
|
83599
83660
|
const timer = setTimeout(() => {
|
|
83600
83661
|
signal?.removeEventListener("abort", onAbort);
|
|
83601
|
-
|
|
83662
|
+
resolve12();
|
|
83602
83663
|
}, ms3);
|
|
83603
83664
|
const onAbort = () => {
|
|
83604
83665
|
clearTimeout(timer);
|
|
@@ -84392,7 +84453,7 @@ var Protocol2 = class {
|
|
|
84392
84453
|
const flowStartedAt = Date.now();
|
|
84393
84454
|
let onAbort;
|
|
84394
84455
|
let cleanupMessageId;
|
|
84395
|
-
return new Promise((
|
|
84456
|
+
return new Promise((resolve12, reject) => {
|
|
84396
84457
|
const earlyReject = (error51) => {
|
|
84397
84458
|
reject(error51);
|
|
84398
84459
|
};
|
|
@@ -84461,7 +84522,7 @@ var Protocol2 = class {
|
|
|
84461
84522
|
}
|
|
84462
84523
|
if (decoded.kind === "invalid") return reject(decoded.error);
|
|
84463
84524
|
if (decoded.kind === "input_required") {
|
|
84464
|
-
if (options?.allowInputRequired === true) return
|
|
84525
|
+
if (options?.allowInputRequired === true) return resolve12(manualInputRequiredValue2(decoded));
|
|
84465
84526
|
const flow = {
|
|
84466
84527
|
codec: codec2,
|
|
84467
84528
|
request,
|
|
@@ -84473,11 +84534,11 @@ var Protocol2 = class {
|
|
|
84473
84534
|
params
|
|
84474
84535
|
}, resultSchema, legOptions)
|
|
84475
84536
|
};
|
|
84476
|
-
return
|
|
84537
|
+
return resolve12(this._resolveNonCompleteResult(decoded, flow));
|
|
84477
84538
|
}
|
|
84478
84539
|
const result = decoded.result;
|
|
84479
84540
|
validateStandardSchema2(resultSchema, result).then((parseResult) => {
|
|
84480
|
-
if (parseResult.success)
|
|
84541
|
+
if (parseResult.success) resolve12(parseResult.data);
|
|
84481
84542
|
else reject(new SdkError2(SdkErrorCode2.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
|
|
84482
84543
|
}, reject);
|
|
84483
84544
|
});
|
|
@@ -87389,7 +87450,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin2(((exports) => {
|
|
|
87389
87450
|
ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
|
|
87390
87451
|
const schOrFunc = root3.refs[ref];
|
|
87391
87452
|
if (schOrFunc) return schOrFunc;
|
|
87392
|
-
let _sch =
|
|
87453
|
+
let _sch = resolve12.call(this, root3, ref);
|
|
87393
87454
|
if (_sch === void 0) {
|
|
87394
87455
|
const schema = (_a4 = root3.localRefs) === null || _a4 === void 0 ? void 0 : _a4[ref];
|
|
87395
87456
|
const { schemaId } = this.opts;
|
|
@@ -87415,7 +87476,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin2(((exports) => {
|
|
|
87415
87476
|
function sameSchemaEnv(s12, s22) {
|
|
87416
87477
|
return s12.schema === s22.schema && s12.root === s22.root && s12.baseId === s22.baseId;
|
|
87417
87478
|
}
|
|
87418
|
-
function
|
|
87479
|
+
function resolve12(root3, ref) {
|
|
87419
87480
|
let sch;
|
|
87420
87481
|
while (typeof (sch = this.refs[ref]) == "string") ref = sch;
|
|
87421
87482
|
return sch || this.schemas[ref] || resolveSchema.call(this, root3, ref);
|
|
@@ -87865,7 +87926,7 @@ var require_fast_uri2 = /* @__PURE__ */ __commonJSMin2(((exports, module) => {
|
|
|
87865
87926
|
else if (typeof uri === "object") uri = parse7(serialize(uri, options), options);
|
|
87866
87927
|
return uri;
|
|
87867
87928
|
}
|
|
87868
|
-
function
|
|
87929
|
+
function resolve12(baseURI, relativeURI, options) {
|
|
87869
87930
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
87870
87931
|
const resolved = resolveComponent(parse7(baseURI, schemelessOptions), parse7(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
87871
87932
|
schemelessOptions.skipEscape = true;
|
|
@@ -88039,7 +88100,7 @@ var require_fast_uri2 = /* @__PURE__ */ __commonJSMin2(((exports, module) => {
|
|
|
88039
88100
|
const fastUri = {
|
|
88040
88101
|
SCHEMES,
|
|
88041
88102
|
normalize: normalize2,
|
|
88042
|
-
resolve:
|
|
88103
|
+
resolve: resolve12,
|
|
88043
88104
|
resolveComponent,
|
|
88044
88105
|
equal,
|
|
88045
88106
|
serialize,
|
|
@@ -93472,7 +93533,7 @@ var StdioServerTransport = class {
|
|
|
93472
93533
|
}
|
|
93473
93534
|
send(message) {
|
|
93474
93535
|
if (this._closed) return Promise.reject(/* @__PURE__ */ new Error("StdioServerTransport is closed"));
|
|
93475
|
-
return new Promise((
|
|
93536
|
+
return new Promise((resolve12, reject) => {
|
|
93476
93537
|
const json2 = serializeMessage2(message);
|
|
93477
93538
|
let settled = false;
|
|
93478
93539
|
const onError = (error51) => {
|
|
@@ -93487,14 +93548,14 @@ var StdioServerTransport = class {
|
|
|
93487
93548
|
settled = true;
|
|
93488
93549
|
this._stdout.off("error", onError);
|
|
93489
93550
|
this._stdout.off("drain", onDrain2);
|
|
93490
|
-
|
|
93551
|
+
resolve12();
|
|
93491
93552
|
};
|
|
93492
93553
|
this._stdout.once("error", onError);
|
|
93493
93554
|
if (this._stdout.write(json2)) {
|
|
93494
93555
|
if (settled) return;
|
|
93495
93556
|
settled = true;
|
|
93496
93557
|
this._stdout.off("error", onError);
|
|
93497
|
-
|
|
93558
|
+
resolve12();
|
|
93498
93559
|
} else if (!settled) this._stdout.once("drain", onDrain2);
|
|
93499
93560
|
});
|
|
93500
93561
|
}
|
|
@@ -93548,14 +93609,14 @@ var StdioConnectionChannel = class {
|
|
|
93548
93609
|
*/
|
|
93549
93610
|
async whenRequestsAnswered(timeoutMs) {
|
|
93550
93611
|
if (this._closed || this._pendingRequests.size === 0) return true;
|
|
93551
|
-
return await new Promise((
|
|
93612
|
+
return await new Promise((resolve12) => {
|
|
93552
93613
|
const waiter = () => {
|
|
93553
93614
|
clearTimeout(timer);
|
|
93554
|
-
|
|
93615
|
+
resolve12(true);
|
|
93555
93616
|
};
|
|
93556
93617
|
const timer = setTimeout(() => {
|
|
93557
93618
|
this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
|
|
93558
|
-
|
|
93619
|
+
resolve12(false);
|
|
93559
93620
|
}, timeoutMs);
|
|
93560
93621
|
this._drainWaiters.push(waiter);
|
|
93561
93622
|
});
|
|
@@ -94468,7 +94529,7 @@ var BrokerDesk = class {
|
|
|
94468
94529
|
try {
|
|
94469
94530
|
answer = await Promise.race([
|
|
94470
94531
|
asked.answer,
|
|
94471
|
-
new Promise((
|
|
94532
|
+
new Promise((resolve12) => call2.signal.aborted ? resolve12(null) : call2.signal.addEventListener("abort", () => resolve12(null), { once: true }))
|
|
94472
94533
|
]);
|
|
94473
94534
|
} finally {
|
|
94474
94535
|
call2.signal.removeEventListener("abort", leave);
|
|
@@ -96392,6 +96453,11 @@ var ExternalApps = class {
|
|
|
96392
96453
|
}
|
|
96393
96454
|
return out;
|
|
96394
96455
|
}
|
|
96456
|
+
sessionBoundaryOpen = () => false;
|
|
96457
|
+
/** Host-owned policy; a request cannot claim an auto preset in its arguments. */
|
|
96458
|
+
setSessionBoundaryPolicy(policy) {
|
|
96459
|
+
this.sessionBoundaryOpen = policy;
|
|
96460
|
+
}
|
|
96395
96461
|
/**
|
|
96396
96462
|
* An app's tool list — if the app is down, **this is where it starts** (the first time it is
|
|
96397
96463
|
* needed).
|
|
@@ -96400,9 +96466,9 @@ var ExternalApps = class {
|
|
|
96400
96466
|
* read is the place it is enforced: this list becomes the source of truth both for what attaches to
|
|
96401
96467
|
* a session (A-5) and for the tools a screen can call.
|
|
96402
96468
|
*/
|
|
96403
|
-
async tools(ref, audience) {
|
|
96469
|
+
async tools(ref, audience, sessionId) {
|
|
96404
96470
|
const e = this.require(ref);
|
|
96405
|
-
const all = await this.use(e, async () => e.life.tools ?? []);
|
|
96471
|
+
const all = await this.use(e, async () => e.life.tools ?? [], !!sessionId && this.sessionBoundaryOpen(sessionId));
|
|
96406
96472
|
return all.filter((t) => !audience || t.visibility.includes(audience)).map((t) => t.tool);
|
|
96407
96473
|
}
|
|
96408
96474
|
/**
|
|
@@ -96534,7 +96600,8 @@ var ExternalApps = class {
|
|
|
96534
96600
|
return { runId, status, result, error: error51, durationMs };
|
|
96535
96601
|
};
|
|
96536
96602
|
if (!e.manifest) return done("rejected", null, `This app's manifest is invalid: ${e.error}`);
|
|
96537
|
-
|
|
96603
|
+
const open5 = caller.kind === "session" && this.sessionBoundaryOpen(caller.sessionId);
|
|
96604
|
+
if (!e.scope.trusted && !open5) return done("rejected", null, "This app's project is not trusted, so Centralu does not call its apps");
|
|
96538
96605
|
const held3 = this.held(e);
|
|
96539
96606
|
if (held3) return done("rejected", null, held3);
|
|
96540
96607
|
if (e.life.gaveUp) return done("rejected", null, `This app stopped after failing ${this.timing.maxFailures} times in a row`);
|
|
@@ -96582,7 +96649,7 @@ var ExternalApps = class {
|
|
|
96582
96649
|
abort.abort();
|
|
96583
96650
|
for (const sig of upstream) sig.removeEventListener("abort", onUp);
|
|
96584
96651
|
}
|
|
96585
|
-
});
|
|
96652
|
+
}, open5);
|
|
96586
96653
|
} catch (err) {
|
|
96587
96654
|
return done("error", null, err.message);
|
|
96588
96655
|
}
|
|
@@ -97186,11 +97253,11 @@ var ExternalApps = class {
|
|
|
97186
97253
|
}
|
|
97187
97254
|
// ── Lifecycle ──────────────────────────────────────────────────────────────────────
|
|
97188
97255
|
/** An app is never idle while in use — the idle timer is set again once it is done */
|
|
97189
|
-
async use(e, fn2) {
|
|
97256
|
+
async use(e, fn2, boundaryOpen = false) {
|
|
97190
97257
|
e.life.inflight += 1;
|
|
97191
97258
|
this.clearIdle(e);
|
|
97192
97259
|
try {
|
|
97193
|
-
return await fn2(await this.ensureRunning(e));
|
|
97260
|
+
return await fn2(await this.ensureRunning(e, boundaryOpen));
|
|
97194
97261
|
} finally {
|
|
97195
97262
|
e.life.inflight -= 1;
|
|
97196
97263
|
if (e.life.inflight === 0) for (const w of e.life.idleWaiters.splice(0)) w();
|
|
@@ -97204,14 +97271,14 @@ var ExternalApps = class {
|
|
|
97204
97271
|
*/
|
|
97205
97272
|
drain(e, ms3) {
|
|
97206
97273
|
if (e.life.inflight === 0) return Promise.resolve(true);
|
|
97207
|
-
return new Promise((
|
|
97274
|
+
return new Promise((resolve12) => {
|
|
97208
97275
|
const waiter = () => {
|
|
97209
97276
|
clearTimeout(t);
|
|
97210
|
-
|
|
97277
|
+
resolve12(true);
|
|
97211
97278
|
};
|
|
97212
97279
|
const t = setTimeout(() => {
|
|
97213
97280
|
e.life.idleWaiters = e.life.idleWaiters.filter((w) => w !== waiter);
|
|
97214
|
-
|
|
97281
|
+
resolve12(false);
|
|
97215
97282
|
}, ms3);
|
|
97216
97283
|
t.unref();
|
|
97217
97284
|
e.life.idleWaiters.push(waiter);
|
|
@@ -97225,10 +97292,10 @@ var ExternalApps = class {
|
|
|
97225
97292
|
* encountered restoring sessions (see the `resuming` comment in `manager.ts`). Everyone waits on the
|
|
97226
97293
|
* same in-progress startup promise instead.
|
|
97227
97294
|
*/
|
|
97228
|
-
ensureRunning(e) {
|
|
97295
|
+
ensureRunning(e, boundaryOpen = false) {
|
|
97229
97296
|
const L2 = e.life;
|
|
97230
97297
|
if (!e.manifest) throw new AppUnavailableError(`The app cannot start \u2014 its manifest is invalid: ${e.error}`);
|
|
97231
|
-
if (!e.scope.trusted) {
|
|
97298
|
+
if (!e.scope.trusted && !boundaryOpen) {
|
|
97232
97299
|
throw new AppUnavailableError("This app's project is not trusted, so the app does not start \u2014 trust the project and it will");
|
|
97233
97300
|
}
|
|
97234
97301
|
const held3 = this.held(e);
|
|
@@ -98030,12 +98097,12 @@ function readBook(raw) {
|
|
|
98030
98097
|
return book;
|
|
98031
98098
|
}
|
|
98032
98099
|
function listenOn(port2, handler) {
|
|
98033
|
-
return new Promise((
|
|
98100
|
+
return new Promise((resolve12, reject) => {
|
|
98034
98101
|
const server2 = createServer2(handler);
|
|
98035
98102
|
server2.once("error", reject);
|
|
98036
98103
|
server2.listen(port2, "127.0.0.1", () => {
|
|
98037
98104
|
server2.removeListener("error", reject);
|
|
98038
|
-
|
|
98105
|
+
resolve12(server2);
|
|
98039
98106
|
});
|
|
98040
98107
|
});
|
|
98041
98108
|
}
|
|
@@ -98103,8 +98170,155 @@ var OriginPorts = class {
|
|
|
98103
98170
|
}
|
|
98104
98171
|
};
|
|
98105
98172
|
|
|
98173
|
+
// packages/agent-host/src/dev-services/worktree-recovery.ts
|
|
98174
|
+
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";
|
|
98177
|
+
import { join as join16, resolve as resolve2 } from "node:path";
|
|
98178
|
+
var WORKTREE_RECOVERY_MAX_BYTES = 256 * 1024 * 1024;
|
|
98179
|
+
var RECOVERY_TIMEOUT = 12e4;
|
|
98180
|
+
function worktreeRecoveryDir(sessionId) {
|
|
98181
|
+
if (!isSessionId(sessionId)) throw new Error("Invalid session id for worktree recovery");
|
|
98182
|
+
return join16(dataRoot(), "worktree-recovery", sessionId);
|
|
98183
|
+
}
|
|
98184
|
+
async function clearWorktreeRecovery(sessionId) {
|
|
98185
|
+
await rm2(worktreeRecoveryDir(sessionId), { recursive: true, force: true });
|
|
98186
|
+
}
|
|
98187
|
+
async function saveWorktreeRecovery(sessionId, cwd, trunk, branch) {
|
|
98188
|
+
const plans = [];
|
|
98189
|
+
let bytes2 = 0;
|
|
98190
|
+
const account = (size) => {
|
|
98191
|
+
bytes2 += size;
|
|
98192
|
+
if (bytes2 > WORKTREE_RECOVERY_MAX_BYTES) {
|
|
98193
|
+
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
|
+
}
|
|
98195
|
+
};
|
|
98196
|
+
const prepare = async (repo, path2) => {
|
|
98197
|
+
const tip = (await runGit(repo, ["rev-parse", "--verify", "HEAD"])).trim();
|
|
98198
|
+
const refs = ["HEAD"];
|
|
98199
|
+
if (!path2 && branch) {
|
|
98200
|
+
const ref = `refs/heads/${branch}`;
|
|
98201
|
+
if (await runGit(repo, ["rev-parse", "--verify", ref]).then(() => true).catch(() => false)) refs.push(ref);
|
|
98202
|
+
} else if (path2) {
|
|
98203
|
+
refs.push(...(await runGit(repo, ["for-each-ref", "--format=%(refname)", "refs/heads/"])).trim().split("\n").filter(Boolean));
|
|
98204
|
+
}
|
|
98205
|
+
const baselineOid = !path2 ? await runGit(repo, ["rev-parse", "--verify", `${trunk}^{commit}`]).then((s) => s.trim()).catch(() => null) : null;
|
|
98206
|
+
const exclude = path2 ? ["--not", "--remotes"] : baselineOid ? ["--not", baselineOid] : [];
|
|
98207
|
+
const commits = Number((await runGit(repo, ["rev-list", "--count", ...refs, ...exclude])).trim());
|
|
98208
|
+
plans.push({ repo, path: path2, tip, refs, commits });
|
|
98209
|
+
const stagedNames = new Set((await runGit(repo, ["diff", "--cached", "--name-only", "-z", "--no-renames", tip], { timeout: RECOVERY_TIMEOUT })).split("\0"));
|
|
98210
|
+
const modules = [];
|
|
98211
|
+
const stagedObjects = [];
|
|
98212
|
+
for (const row of (await runGit(repo, ["ls-files", "--stage", "-z"])).split("\0")) {
|
|
98213
|
+
if (!row) continue;
|
|
98214
|
+
const tab = row.indexOf(" ");
|
|
98215
|
+
const rel = row.slice(tab + 1);
|
|
98216
|
+
if (row.startsWith("160000 ")) modules.push(rel);
|
|
98217
|
+
else if (stagedNames.has(rel)) stagedObjects.push(row.split(" ")[1]);
|
|
98218
|
+
}
|
|
98219
|
+
if (stagedObjects.length) {
|
|
98220
|
+
const sizes = await runGit(repo, ["cat-file", "--batch-check=%(objectsize)"], { input: stagedObjects.join("\n") + "\n" });
|
|
98221
|
+
for (const size of sizes.trim().split("\n")) {
|
|
98222
|
+
if (!/^\d+$/.test(size)) throw new Error("Could not measure staged recovery data");
|
|
98223
|
+
account(Number(size));
|
|
98224
|
+
}
|
|
98225
|
+
}
|
|
98226
|
+
const files = new Set((await runGit(repo, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"])).split("\0").filter(Boolean));
|
|
98227
|
+
for (const rel of files) {
|
|
98228
|
+
const file2 = await lstat3(join16(repo, rel)).catch((e) => {
|
|
98229
|
+
if (e.code !== "ENOENT") throw e;
|
|
98230
|
+
return null;
|
|
98231
|
+
});
|
|
98232
|
+
if (file2?.isFile() || file2?.isSymbolicLink()) account(file2.size);
|
|
98233
|
+
else if (file2?.isDirectory() && existsSync13(join16(repo, rel, ".git"))) modules.push(rel);
|
|
98234
|
+
}
|
|
98235
|
+
for (const rel of new Set(modules)) {
|
|
98236
|
+
await assertExistingPath(repo, rel).catch((e) => {
|
|
98237
|
+
if (existsSync13(join16(repo, rel))) throw e;
|
|
98238
|
+
});
|
|
98239
|
+
if (existsSync13(join16(repo, rel, ".git"))) await prepare(join16(repo, rel), path2 ? `${path2}/${rel}` : rel);
|
|
98240
|
+
}
|
|
98241
|
+
};
|
|
98242
|
+
await prepare(cwd, "");
|
|
98243
|
+
const snapshotId = randomUUID6();
|
|
98244
|
+
const dir = join16(worktreeRecoveryDir(sessionId), snapshotId);
|
|
98245
|
+
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
98246
|
+
const snapshot2 = { snapshot: snapshotId, tip: plans[0].tip, changedFiles: 0, commits: 0, repositories: [] };
|
|
98247
|
+
const patchFile = async (repo, tip, file2, env) => {
|
|
98248
|
+
await runGit(repo, ["diff", "--cached", "--binary", "--full-index", "--no-renames", `--output=${file2}`, tip], { env, timeout: RECOVERY_TIMEOUT });
|
|
98249
|
+
await chmod(file2, 384);
|
|
98250
|
+
if ((await stat3(file2)).size > 0) return true;
|
|
98251
|
+
await rm2(file2);
|
|
98252
|
+
return false;
|
|
98253
|
+
};
|
|
98254
|
+
try {
|
|
98255
|
+
for (const [n, plan] of plans.entries()) {
|
|
98256
|
+
const { repo, path: path2, tip, refs, commits } = plan;
|
|
98257
|
+
const entry = { path: path2, tip, refs, patch: false, staged: false, bundle: commits > 0 };
|
|
98258
|
+
snapshot2.repositories.push(entry);
|
|
98259
|
+
snapshot2.commits += commits;
|
|
98260
|
+
if (entry.bundle) await runGit(repo, ["bundle", "create", join16(dir, `${n}.bundle`), ...refs], { timeout: RECOVERY_TIMEOUT });
|
|
98261
|
+
entry.staged = await patchFile(repo, tip, join16(dir, `${n}.staged.patch`));
|
|
98262
|
+
const stagedNames = await runGit(repo, ["diff", "--cached", "--name-only", "-z", "--no-renames", tip], { timeout: RECOVERY_TIMEOUT });
|
|
98263
|
+
const env = { GIT_INDEX_FILE: join16(dir, `${n}.index`) };
|
|
98264
|
+
try {
|
|
98265
|
+
await runGit(repo, ["read-tree", tip], { env });
|
|
98266
|
+
await runGit(repo, ["add", "-A", "--", "."], { env, timeout: RECOVERY_TIMEOUT });
|
|
98267
|
+
const names = await runGit(repo, ["diff", "--cached", "--name-only", "-z", "--no-renames", tip], { env, timeout: RECOVERY_TIMEOUT });
|
|
98268
|
+
snapshot2.changedFiles += new Set([...names.split("\0"), ...stagedNames.split("\0")].filter(Boolean)).size;
|
|
98269
|
+
entry.patch = await patchFile(repo, tip, join16(dir, `${n}.patch`), env);
|
|
98270
|
+
} finally {
|
|
98271
|
+
await rm2(env.GIT_INDEX_FILE, { force: true });
|
|
98272
|
+
}
|
|
98273
|
+
}
|
|
98274
|
+
await writeFile2(join16(dir, "snapshot.json"), JSON.stringify(snapshot2), { mode: 384 });
|
|
98275
|
+
return { snapshot: snapshotId, tip: snapshot2.tip, changedFiles: snapshot2.changedFiles, commits: snapshot2.commits };
|
|
98276
|
+
} catch (e) {
|
|
98277
|
+
await rm2(dir, { recursive: true, force: true });
|
|
98278
|
+
throw e;
|
|
98279
|
+
}
|
|
98280
|
+
}
|
|
98281
|
+
async function worktreeRecoveryBytes(sessionId) {
|
|
98282
|
+
const dir = worktreeRecoveryDir(sessionId);
|
|
98283
|
+
if (!existsSync13(dir)) return 0;
|
|
98284
|
+
const sizes = await Promise.all((await readdir2(dir, { recursive: true })).map(async (name) => {
|
|
98285
|
+
const s = await stat3(join16(dir, name));
|
|
98286
|
+
return s.isFile() ? s.size : 0;
|
|
98287
|
+
}));
|
|
98288
|
+
return sizes.reduce((a, b2) => a + b2, 0);
|
|
98289
|
+
}
|
|
98290
|
+
async function restoreWorktreeRecovery(sessionId, repo, path2, branch, recovery, trust) {
|
|
98291
|
+
if (!isSessionId(recovery.snapshot)) throw new Error("Invalid recovery snapshot");
|
|
98292
|
+
const dir = join16(worktreeRecoveryDir(sessionId), recovery.snapshot);
|
|
98293
|
+
const snapshot2 = JSON.parse(await readFile(join16(dir, "snapshot.json"), "utf8"));
|
|
98294
|
+
const root3 = snapshot2.repositories[0];
|
|
98295
|
+
if (!existsSync13(path2)) {
|
|
98296
|
+
if (await runGit(repo, ["show-ref", "--verify", `refs/heads/${branch}`]).then(() => true).catch(() => false)) {
|
|
98297
|
+
branch = `recovered/${sessionId}`;
|
|
98298
|
+
}
|
|
98299
|
+
if (root3.bundle) await runGit(repo, ["fetch", join16(dir, "0.bundle"), ...root3.refs ?? ["HEAD"]], { allowRepoPrograms: trust.trusted === true });
|
|
98300
|
+
await runGit(repo, ["worktree", "add", "-b", branch, path2, recovery.tip], { allowRepoPrograms: trust.trusted === true });
|
|
98301
|
+
if (root3.patch || root3.staged) {
|
|
98302
|
+
try {
|
|
98303
|
+
if (root3.staged) await runGit(path2, ["apply", "--cached", "--binary", join16(dir, "0.staged.patch")]);
|
|
98304
|
+
if (root3.patch) await runGit(path2, ["apply", "--binary", join16(dir, "0.patch")]);
|
|
98305
|
+
} 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.` };
|
|
98307
|
+
}
|
|
98308
|
+
}
|
|
98309
|
+
} else {
|
|
98310
|
+
const common = async (cwd) => realpath3(resolve2(cwd, (await runGit(cwd, ["rev-parse", "--git-common-dir"])).trim()));
|
|
98311
|
+
if (await common(path2) !== await common(repo)) throw new Error(`The worktree path belongs to another repository. Recovery files remain at ${dir}`);
|
|
98312
|
+
branch = (await runGit(path2, ["symbolic-ref", "--quiet", "--short", "HEAD"])).trim();
|
|
98313
|
+
}
|
|
98314
|
+
return {
|
|
98315
|
+
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.`
|
|
98317
|
+
};
|
|
98318
|
+
}
|
|
98319
|
+
|
|
98106
98320
|
// packages/agent-host/src/sessions/manager.ts
|
|
98107
|
-
import { randomUUID as
|
|
98321
|
+
import { randomUUID as randomUUID10 } from "node:crypto";
|
|
98108
98322
|
|
|
98109
98323
|
// packages/agent-host/src/sessions/tool-cost.ts
|
|
98110
98324
|
import { createHash as createHash8 } from "node:crypto";
|
|
@@ -98422,8 +98636,8 @@ var READER_TOOLS = [
|
|
|
98422
98636
|
var DELEGATE_TOOLS = [
|
|
98423
98637
|
{
|
|
98424
98638
|
name: "ask_project",
|
|
98425
|
-
description: "
|
|
98426
|
-
schema: external_exports.object({ project: external_exports.string(), task: external_exports.string().optional() })
|
|
98639
|
+
description: "Asks another project's existing session; returns its answer. For another project's apps, use find_apps.",
|
|
98640
|
+
schema: external_exports.object({ project: external_exports.string(), session: external_exports.string().optional(), task: external_exports.string().optional() })
|
|
98427
98641
|
}
|
|
98428
98642
|
];
|
|
98429
98643
|
var DELEGATE_BUDGET_CHARS = 400;
|
|
@@ -98547,7 +98761,7 @@ function askProjectText(r) {
|
|
|
98547
98761
|
const where = `the session "${r.sessionName}" [${r.sessionId}] in ${r.project}`;
|
|
98548
98762
|
if (r.state === "working") {
|
|
98549
98763
|
return {
|
|
98550
|
-
text: `${r.project} is still working on it in ${where}${r.notice ? ` \u2014 ${r.notice}` : ""}. To keep waiting, call ask_project again with project "${r.project}" and no task; the work goes on meanwhile.`
|
|
98764
|
+
text: `${r.project} is still working on it in ${where}${r.notice ? ` \u2014 ${r.notice}` : ""}. To keep waiting, call ask_project again with project "${r.project}", session "${r.sessionId}" and no task; the work goes on meanwhile.`
|
|
98551
98765
|
};
|
|
98552
98766
|
}
|
|
98553
98767
|
const lines = [`${r.project} answered (from ${where}), as JSON:`, trustedJsonText(r.answer || "(no final text)")];
|
|
@@ -98567,7 +98781,7 @@ function agentRunText(r) {
|
|
|
98567
98781
|
return { text: `Stopped ${who}.` };
|
|
98568
98782
|
case "deleted":
|
|
98569
98783
|
return {
|
|
98570
|
-
text: `
|
|
98784
|
+
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."
|
|
98571
98785
|
};
|
|
98572
98786
|
case "at_rest":
|
|
98573
98787
|
return {
|
|
@@ -98603,7 +98817,8 @@ async function runOrchestratorTool(tools, name, args, caller = { sessionId: null
|
|
|
98603
98817
|
const project2 = typeof args.project === "string" ? args.project.trim() : "";
|
|
98604
98818
|
if (!project2) return { text: "Give a project \u2014 the name of another Centralu project.", isError: true };
|
|
98605
98819
|
const task = typeof args.task === "string" && args.task.trim() ? args.task : void 0;
|
|
98606
|
-
|
|
98820
|
+
const session2 = typeof args.session === "string" && args.session.trim() ? args.session.trim() : void 0;
|
|
98821
|
+
return askProjectText(await tools.askProject({ project: project2, ...session2 ? { session: session2 } : {}, ...task ? { task } : {} }, signal));
|
|
98607
98822
|
}
|
|
98608
98823
|
if (name === "start_agent" || name === "agent_session") {
|
|
98609
98824
|
if (caller.profile !== "reader") return { text: `Not a tool of this session: ${name}`, isError: true };
|
|
@@ -99015,7 +99230,7 @@ function shortHash(value) {
|
|
|
99015
99230
|
|
|
99016
99231
|
// packages/agent-host/src/sessions/orchestrator-home.ts
|
|
99017
99232
|
import { mkdirSync as mkdirSync7 } from "node:fs";
|
|
99018
|
-
import { join as
|
|
99233
|
+
import { join as join17 } from "node:path";
|
|
99019
99234
|
var preamble = true;
|
|
99020
99235
|
function setOrchestratorPreamble(on2) {
|
|
99021
99236
|
preamble = on2;
|
|
@@ -99031,7 +99246,7 @@ function orchestratorRole() {
|
|
|
99031
99246
|
${ORCHESTRATOR_ROLE}` : ORCHESTRATOR_ROLE;
|
|
99032
99247
|
}
|
|
99033
99248
|
function orchestratorHome() {
|
|
99034
|
-
const dir =
|
|
99249
|
+
const dir = join17(dataRoot(), "orchestrator");
|
|
99035
99250
|
mkdirSync7(dir, { recursive: true });
|
|
99036
99251
|
return dir;
|
|
99037
99252
|
}
|
|
@@ -99579,7 +99794,7 @@ var SessionAppsHub = class {
|
|
|
99579
99794
|
* keep answering "yes" even after trust is revoked.
|
|
99580
99795
|
*/
|
|
99581
99796
|
refsFor(session2) {
|
|
99582
|
-
const usable = this.rt.list().filter((a) => !UNUSABLE.has(a.status));
|
|
99797
|
+
const usable = this.rt.list().filter((a) => !UNUSABLE.has(a.status) || a.status === "untrusted" && !!this.opts.boundaryOpen?.(session2.id));
|
|
99583
99798
|
const hits = usable.filter((a) => givesApp(session2, a)).map((a) => ({ ref: { projectId: a.projectId, appId: a.appId }, server: appMcpServerName(a.appId) })).sort((x, y) => x.server.localeCompare(y.server));
|
|
99584
99799
|
const taken = new Set(hits.map((h) => h.server));
|
|
99585
99800
|
for (const x of this.opts.onDemand?.attached(session2.id) ?? []) {
|
|
@@ -99606,8 +99821,8 @@ var SessionAppsHub = class {
|
|
|
99606
99821
|
const own = session2?.id ? this.opts.onDemand?.attached(session2.id).find((x) => sameRef(x.ref, ref)) : void 0;
|
|
99607
99822
|
const onDemand = !!session2 && !!own && this.opts.onDemand.allowed(session2, app2);
|
|
99608
99823
|
if (!session2 || !givesApp(session2, app2) && !onDemand) return { reachable: false, reason: "other-project" };
|
|
99609
|
-
if (app2.status === "untrusted") return { reachable: false, reason: "untrusted" };
|
|
99610
|
-
if (UNUSABLE.has(app2.status)) return { reachable: false, reason: "app-unusable", status: app2.status };
|
|
99824
|
+
if (app2.status === "untrusted" && !this.opts.boundaryOpen?.(session2.id)) return { reachable: false, reason: "untrusted" };
|
|
99825
|
+
if (UNUSABLE.has(app2.status) && !(app2.status === "untrusted" && this.opts.boundaryOpen?.(session2.id))) return { reachable: false, reason: "app-unusable", status: app2.status };
|
|
99611
99826
|
const server2 = givesApp(session2, app2) ? appMcpServerName(app2.appId) : own.server;
|
|
99612
99827
|
const attached = live?.appAttachment?.(server2) ?? "attached";
|
|
99613
99828
|
if (attached === "restart") return { reachable: false, reason: "restart" };
|
|
@@ -99689,7 +99904,7 @@ var Attachment2 = class {
|
|
|
99689
99904
|
let timer;
|
|
99690
99905
|
try {
|
|
99691
99906
|
const listed = await Promise.race([
|
|
99692
|
-
this.hub.rt.tools(hit.ref, "model"),
|
|
99907
|
+
this.hub.rt.tools(hit.ref, "model", this.session.id),
|
|
99693
99908
|
new Promise((_, reject) => {
|
|
99694
99909
|
timer = setTimeout(() => reject(new Error(`did not list its tools within ${Math.round(waitMs / 1e3)}s`)), waitMs);
|
|
99695
99910
|
})
|
|
@@ -99734,7 +99949,7 @@ var Attachment2 = class {
|
|
|
99734
99949
|
let timer;
|
|
99735
99950
|
const first = await Promise.race([
|
|
99736
99951
|
pending.then((o) => ({ o })),
|
|
99737
|
-
new Promise((
|
|
99952
|
+
new Promise((resolve12) => timer = setTimeout(() => resolve12(null), opts.waitMs))
|
|
99738
99953
|
]);
|
|
99739
99954
|
clearTimeout(timer);
|
|
99740
99955
|
if (first) return toResult(first.o);
|
|
@@ -99840,15 +100055,15 @@ The call has not stopped and carries on. Do not call it again: pass this id as r
|
|
|
99840
100055
|
if (i !== -1) return Promise.resolve(this.noted.splice(i, 1)[0].callId);
|
|
99841
100056
|
if (this.closed) return Promise.resolve(null);
|
|
99842
100057
|
const waitMs = this.hub.opts.callJoinWaitMs ?? CALL_JOIN_WAIT_MS;
|
|
99843
|
-
return new Promise((
|
|
100058
|
+
return new Promise((resolve12) => {
|
|
99844
100059
|
const waiter = {
|
|
99845
100060
|
server: server2,
|
|
99846
100061
|
tool,
|
|
99847
100062
|
args: key,
|
|
99848
|
-
resolve:
|
|
100063
|
+
resolve: resolve12,
|
|
99849
100064
|
timer: setTimeout(() => {
|
|
99850
100065
|
this.waiters = this.waiters.filter((x) => x !== waiter);
|
|
99851
|
-
|
|
100066
|
+
resolve12(null);
|
|
99852
100067
|
}, waitMs)
|
|
99853
100068
|
};
|
|
99854
100069
|
waiter.timer.unref?.();
|
|
@@ -99959,12 +100174,12 @@ var AppAccess = class {
|
|
|
99959
100174
|
}
|
|
99960
100175
|
allowed(session2, app2) {
|
|
99961
100176
|
const from = session2.projectId;
|
|
99962
|
-
if (from === null || !this.projectTrusted(from)) return false;
|
|
100177
|
+
if (from === null || !this.projectTrusted(from) && !this.deps.boundaryOpen?.(session2.id)) return false;
|
|
99963
100178
|
if (app2.projectId === null) return true;
|
|
99964
100179
|
if (app2.projectId === from || app2.shared !== true) return false;
|
|
99965
100180
|
const r = this.records(session2.id).find((x) => x.appId === app2.appId && x.projectId === app2.projectId);
|
|
99966
100181
|
if (!r) return false;
|
|
99967
|
-
return r.consent === "once" || !!this.deps.store.getProjectConsent(from, app2.projectId, "apps");
|
|
100182
|
+
return !!this.deps.boundaryOpen?.(session2.id) || r.consent === "once" || !!this.deps.store.getProjectConsent(from, app2.projectId, "apps");
|
|
99968
100183
|
}
|
|
99969
100184
|
// ── The three tools ──
|
|
99970
100185
|
find(sessionId, query) {
|
|
@@ -99987,7 +100202,7 @@ var AppAccess = class {
|
|
|
99987
100202
|
const app2 = named.app;
|
|
99988
100203
|
const target = { projectId: app2.projectId, appId: app2.appId };
|
|
99989
100204
|
const has = already.find((h) => h.ref.appId === target.appId && h.ref.projectId === target.projectId);
|
|
99990
|
-
if (has) return { ok: true, server: has.server, tools: await this.toolNames(target), when: "now", already: true };
|
|
100205
|
+
if (has) return { ok: true, server: has.server, tools: await this.toolNames(target, sessionId), when: "now", already: true };
|
|
99991
100206
|
if (!this.candidates(session2).some((a) => a.appId === app2.appId && a.projectId === app2.projectId)) {
|
|
99992
100207
|
return { ok: false, error: this.whyNot(session2, app2) };
|
|
99993
100208
|
}
|
|
@@ -100000,7 +100215,7 @@ var AppAccess = class {
|
|
|
100000
100215
|
signal
|
|
100001
100216
|
);
|
|
100002
100217
|
if (!res.ok) return res;
|
|
100003
|
-
consent = this.deps.store.getProjectConsent(session2.projectId, app2.projectId, "apps") ? "always" : "once";
|
|
100218
|
+
consent = this.deps.boundaryOpen?.(sessionId) ? "auto" : this.deps.store.getProjectConsent(session2.projectId, app2.projectId, "apps") ? "always" : "once";
|
|
100004
100219
|
const again = this.gate(sessionId);
|
|
100005
100220
|
if (!again.ok) return again;
|
|
100006
100221
|
if (!this.candidates(again.session).some((a) => a.appId === app2.appId && a.projectId === app2.projectId)) {
|
|
@@ -100012,7 +100227,7 @@ var AppAccess = class {
|
|
|
100012
100227
|
const server2 = this.freeServer(app2.appId, taken);
|
|
100013
100228
|
this.save(sessionId, [...kept, { projectId: app2.projectId, appId: app2.appId, server: server2, consent, at: Date.now() }]);
|
|
100014
100229
|
const when = this.deps.attachedChanged(sessionId);
|
|
100015
|
-
return { ok: true, server: server2, tools: await this.toolNames(target), when };
|
|
100230
|
+
return { ok: true, server: server2, tools: await this.toolNames(target, sessionId), when };
|
|
100016
100231
|
}
|
|
100017
100232
|
detach(sessionId, ref) {
|
|
100018
100233
|
const mine = this.records(sessionId);
|
|
@@ -100056,7 +100271,7 @@ var AppAccess = class {
|
|
|
100056
100271
|
const session2 = this.deps.session(sessionId);
|
|
100057
100272
|
if (!session2) return { ok: false, error: "This session cannot attach apps." };
|
|
100058
100273
|
if (session2.projectId === null) return { ok: false, error: "Only a session in a project can attach apps from other projects." };
|
|
100059
|
-
if (!this.projectTrusted(session2.projectId)) {
|
|
100274
|
+
if (!this.projectTrusted(session2.projectId) && !this.deps.boundaryOpen?.(sessionId, "attach from an untrusted project")) {
|
|
100060
100275
|
return { ok: false, error: "This session's project is not trusted, so it cannot attach apps \u2014 the person can trust it from the project menu." };
|
|
100061
100276
|
}
|
|
100062
100277
|
return { ok: true, session: session2 };
|
|
@@ -100064,7 +100279,7 @@ var AppAccess = class {
|
|
|
100064
100279
|
/** What this session could attach now: shared apps of other trusted projects, and user-folder apps, minus what it has */
|
|
100065
100280
|
candidates(session2) {
|
|
100066
100281
|
const have = this.deps.hub.refsFor(session2);
|
|
100067
|
-
return this.deps.hub.rt.list().filter((a) => !UNUSABLE2.has(a.status)).filter((a) => a.projectId === null || a.projectId !== session2.projectId && a.shared === true).filter((a) => !have.some((h) => h.ref.appId === a.appId && h.ref.projectId === a.projectId)).sort((x, y) => this.refOf(x).localeCompare(this.refOf(y)));
|
|
100282
|
+
return this.deps.hub.rt.list().filter((a) => !UNUSABLE2.has(a.status) || a.status === "untrusted" && this.deps.boundaryOpen?.(session2.id)).filter((a) => a.projectId === null || a.projectId !== session2.projectId && a.shared === true).filter((a) => !have.some((h) => h.ref.appId === a.appId && h.ref.projectId === a.projectId)).sort((x, y) => this.refOf(x).localeCompare(this.refOf(y)));
|
|
100068
100283
|
}
|
|
100069
100284
|
whyNot(session2, app2) {
|
|
100070
100285
|
if (app2.projectId !== null && app2.projectId === session2.projectId) return `${this.refOf(app2)} is this project's own app; it is attached already when the project is trusted.`;
|
|
@@ -100120,13 +100335,13 @@ var AppAccess = class {
|
|
|
100120
100335
|
for (let n = 2; ; n++) if (!taken.includes(`${base2}-${n}`)) return `${base2}-${n}`;
|
|
100121
100336
|
}
|
|
100122
100337
|
/** The app's agent tools for the tool result — started here if never listed, within the hub's cap; empty on failure */
|
|
100123
|
-
async toolNames(ref) {
|
|
100338
|
+
async toolNames(ref, sessionId) {
|
|
100124
100339
|
const known = this.deps.hub.rt.knownTools(ref, "model");
|
|
100125
100340
|
if (known) return known.map((t) => t.name);
|
|
100126
100341
|
let timer;
|
|
100127
100342
|
try {
|
|
100128
100343
|
const listed = await Promise.race([
|
|
100129
|
-
this.deps.hub.rt.tools(ref, "model"),
|
|
100344
|
+
this.deps.hub.rt.tools(ref, "model", sessionId),
|
|
100130
100345
|
new Promise((_, reject) => timer = setTimeout(() => reject(new Error("timeout")), this.deps.toolListWaitMs ?? TOOL_LIST_WAIT_MS2))
|
|
100131
100346
|
]);
|
|
100132
100347
|
return listed.map((t) => t.name);
|
|
@@ -100160,7 +100375,7 @@ function parse3(raw) {
|
|
|
100160
100375
|
const v = JSON.parse(raw);
|
|
100161
100376
|
if (!Array.isArray(v)) return [];
|
|
100162
100377
|
return v.filter(
|
|
100163
|
-
(x) => !!x && typeof x === "object" && (typeof x.projectId === "string" || x.projectId === null) && typeof x.appId === "string" && typeof x.server === "string" && x.server.startsWith(appMcpServerName("")) && (x.consent === "always" || x.consent === "once" || x.consent === "none")
|
|
100378
|
+
(x) => !!x && typeof x === "object" && (typeof x.projectId === "string" || x.projectId === null) && typeof x.appId === "string" && typeof x.server === "string" && x.server.startsWith(appMcpServerName("")) && (x.consent === "always" || x.consent === "once" || x.consent === "none" || x.consent === "auto")
|
|
100164
100379
|
);
|
|
100165
100380
|
} catch {
|
|
100166
100381
|
return [];
|
|
@@ -100183,17 +100398,19 @@ function finalAnswer(messages2) {
|
|
|
100183
100398
|
}
|
|
100184
100399
|
var AgentRunWait = class {
|
|
100185
100400
|
constructor(notify, sessionName, onUsage = () => {
|
|
100186
|
-
}) {
|
|
100401
|
+
}, readAnswer2) {
|
|
100187
100402
|
this.notify = notify;
|
|
100188
100403
|
this.sessionName = sessionName;
|
|
100189
100404
|
this.onUsage = onUsage;
|
|
100190
|
-
this.
|
|
100405
|
+
this.readAnswer = readAnswer2;
|
|
100406
|
+
this.done = new Promise((resolve12, reject) => this.settle = { resolve: resolve12, reject });
|
|
100191
100407
|
this.done.catch(() => {
|
|
100192
100408
|
});
|
|
100193
100409
|
}
|
|
100194
100410
|
notify;
|
|
100195
100411
|
sessionName;
|
|
100196
100412
|
onUsage;
|
|
100413
|
+
readAnswer;
|
|
100197
100414
|
/** The person stopped that session's turn (`SessionManager.interrupt`) */
|
|
100198
100415
|
stoppedByPerson = false;
|
|
100199
100416
|
/** The person deleted that session — there is no session left to conclude */
|
|
@@ -100201,17 +100418,46 @@ var AgentRunWait = class {
|
|
|
100201
100418
|
done;
|
|
100202
100419
|
settle;
|
|
100203
100420
|
settled = false;
|
|
100421
|
+
inputs = 0;
|
|
100422
|
+
trackedInputs = false;
|
|
100423
|
+
ending;
|
|
100424
|
+
get waitingForInputs() {
|
|
100425
|
+
return this.inputs > 0;
|
|
100426
|
+
}
|
|
100427
|
+
get finished() {
|
|
100428
|
+
return this.settled;
|
|
100429
|
+
}
|
|
100430
|
+
get tracksInputs() {
|
|
100431
|
+
return this.trackedInputs;
|
|
100432
|
+
}
|
|
100433
|
+
get completedAnswer() {
|
|
100434
|
+
return this.ending?.answer;
|
|
100435
|
+
}
|
|
100436
|
+
/** A parent's note may join this turn or run after it. Hold before send, since adapters can emit synchronously. */
|
|
100437
|
+
holdInput() {
|
|
100438
|
+
this.trackedInputs = true;
|
|
100439
|
+
this.inputs++;
|
|
100440
|
+
let released = false;
|
|
100441
|
+
return () => {
|
|
100442
|
+
if (released) return;
|
|
100443
|
+
released = true;
|
|
100444
|
+
this.inputs--;
|
|
100445
|
+
if (!this.inputs && this.ending) this.finish(this.ending);
|
|
100446
|
+
};
|
|
100447
|
+
}
|
|
100204
100448
|
onEvent(e) {
|
|
100205
100449
|
if (e.type === "usage_update") {
|
|
100206
100450
|
const t = e.tokens;
|
|
100207
100451
|
return this.onUsage({ input: t.inputTokens + t.cacheReadTokens + t.cacheCreationTokens, output: t.outputTokens });
|
|
100208
100452
|
}
|
|
100209
100453
|
if (this.settled) return;
|
|
100454
|
+
if (e.type === "message_delta" || e.type === "state_change" && e.state === "working") this.ending = void 0;
|
|
100210
100455
|
switch (e.type) {
|
|
100211
100456
|
case "turn_complete":
|
|
100212
100457
|
if (this.stoppedByPerson) return this.fail(new Error("the person stopped the agent before it finished"));
|
|
100213
|
-
this.
|
|
100214
|
-
|
|
100458
|
+
this.ending = { ...e.output === void 0 ? {} : { output: e.output }, ...this.readAnswer ? { answer: this.readAnswer() } : {} };
|
|
100459
|
+
if (this.inputs) return;
|
|
100460
|
+
return this.finish(this.ending);
|
|
100215
100461
|
case "error":
|
|
100216
100462
|
return this.fail(new Error(this.stoppedByPerson ? "the person stopped the agent before it finished" : `the agent's turn failed: ${e.error.message}`));
|
|
100217
100463
|
case "limit_reached":
|
|
@@ -100225,7 +100471,12 @@ var AgentRunWait = class {
|
|
|
100225
100471
|
fail(err) {
|
|
100226
100472
|
if (this.settled) return;
|
|
100227
100473
|
this.settled = true;
|
|
100228
|
-
this.settle.reject(err);
|
|
100474
|
+
this.settle.reject(this.stoppedByPerson ? new Error("the person stopped the agent before it finished") : err);
|
|
100475
|
+
}
|
|
100476
|
+
finish(ending) {
|
|
100477
|
+
if (this.settled) return;
|
|
100478
|
+
this.settled = true;
|
|
100479
|
+
this.settle.resolve(ending);
|
|
100229
100480
|
}
|
|
100230
100481
|
};
|
|
100231
100482
|
|
|
@@ -100367,7 +100618,7 @@ function windowOf(list, id2, label, since, writes, nameOf) {
|
|
|
100367
100618
|
|
|
100368
100619
|
// packages/agent-host/src/sessions/ask-project.ts
|
|
100369
100620
|
import { realpathSync as realpathSync3, statSync as statSync8 } from "node:fs";
|
|
100370
|
-
import { isAbsolute as isAbsolute4, relative as relative4, resolve as
|
|
100621
|
+
import { isAbsolute as isAbsolute4, relative as relative4, resolve as resolve3, sep as sep4 } from "node:path";
|
|
100371
100622
|
var ASK_WAIT_MS = 24e4;
|
|
100372
100623
|
var ANSWER_MAX_CHARS = 6e3;
|
|
100373
100624
|
var ANSWER_HEAD_CHARS = 4500;
|
|
@@ -100436,7 +100687,7 @@ function readableGrants(paths, projectRoot) {
|
|
|
100436
100687
|
return { granted, outside };
|
|
100437
100688
|
}
|
|
100438
100689
|
function underGrant(path2, grants, cwd) {
|
|
100439
|
-
const abs =
|
|
100690
|
+
const abs = resolve3(cwd, path2);
|
|
100440
100691
|
const r = real(abs) ?? abs;
|
|
100441
100692
|
for (const g5 of grants) {
|
|
100442
100693
|
if (r === g5 || r.startsWith(g5.endsWith(sep4) ? g5 : g5 + sep4)) return true;
|
|
@@ -100457,6 +100708,11 @@ function agentFrame(parentName, task) {
|
|
|
100457
100708
|
task
|
|
100458
100709
|
].join("\n");
|
|
100459
100710
|
}
|
|
100711
|
+
function agentNoteFrame(parentName, text2) {
|
|
100712
|
+
return `[A note from the parent session "${parentName}" through Centralu's agent_session. These are the parent's words about your ongoing work.]
|
|
100713
|
+
|
|
100714
|
+
${text2}`;
|
|
100715
|
+
}
|
|
100460
100716
|
function agentBranch(task, id2, max = 40) {
|
|
100461
100717
|
const slug = task.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, max).replace(/-+$/, "");
|
|
100462
100718
|
return `${APP_SLUG}/${slug || "agent"}-${id2.replace(/[^a-z0-9]/gi, "").slice(0, 4).toLowerCase()}`;
|
|
@@ -100732,13 +100988,79 @@ function orchestratorMemory(reader, sessionId) {
|
|
|
100732
100988
|
|
|
100733
100989
|
// packages/agent-host/src/sessions/manager.ts
|
|
100734
100990
|
import { homedir as homedir12 } from "node:os";
|
|
100735
|
-
import { basename as basename5, isAbsolute as isAbsolute9, join as
|
|
100736
|
-
import { existsSync as
|
|
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";
|
|
100737
100993
|
import { exec as exec6 } from "node:child_process";
|
|
100738
100994
|
|
|
100739
100995
|
// packages/agent-host/src/adapters/claude/index.ts
|
|
100740
100996
|
init_sdk();
|
|
100741
100997
|
import { homedir as homedir5 } from "node:os";
|
|
100998
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
100999
|
+
|
|
101000
|
+
// packages/agent-host/src/adapters/input-receipts.ts
|
|
101001
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
101002
|
+
var InputReceipts = class {
|
|
101003
|
+
pending = /* @__PURE__ */ new Map();
|
|
101004
|
+
ended = /* @__PURE__ */ new Set();
|
|
101005
|
+
create(observer) {
|
|
101006
|
+
const id2 = randomUUID8();
|
|
101007
|
+
let resolve12;
|
|
101008
|
+
let reject;
|
|
101009
|
+
const done = new Promise((yes, no2) => {
|
|
101010
|
+
resolve12 = yes;
|
|
101011
|
+
reject = no2;
|
|
101012
|
+
});
|
|
101013
|
+
done.catch(() => {
|
|
101014
|
+
});
|
|
101015
|
+
const receipt = { id: id2, done, cancelled: false, completed: false };
|
|
101016
|
+
this.pending.set(id2, { receipt, resolve: resolve12, reject, observer });
|
|
101017
|
+
return receipt;
|
|
101018
|
+
}
|
|
101019
|
+
get size() {
|
|
101020
|
+
return this.pending.size;
|
|
101021
|
+
}
|
|
101022
|
+
get ids() {
|
|
101023
|
+
return [...this.pending.keys()];
|
|
101024
|
+
}
|
|
101025
|
+
/** Codex's turn/start response can arrive after that turn's completion notification. */
|
|
101026
|
+
bind(receipt, turn) {
|
|
101027
|
+
const p = this.pending.get(receipt.id);
|
|
101028
|
+
if (!p) return;
|
|
101029
|
+
p.turn = turn;
|
|
101030
|
+
if (this.ended.has(turn)) this.finish(receipt.id);
|
|
101031
|
+
}
|
|
101032
|
+
endTurn(turn) {
|
|
101033
|
+
if (this.pending.size) this.ended.add(turn);
|
|
101034
|
+
for (const [id2, p] of this.pending) if (p.turn === turn) this.finish(id2);
|
|
101035
|
+
}
|
|
101036
|
+
/** Retries can reorder inputs; only UUIDs actually reported as consumed have completed. */
|
|
101037
|
+
complete(ids) {
|
|
101038
|
+
for (const id2 of ids) this.finish(id2);
|
|
101039
|
+
}
|
|
101040
|
+
failAll(error51) {
|
|
101041
|
+
const turns = [...new Set([...this.pending.values()].flatMap((p) => p.turn ? [p.turn] : []))];
|
|
101042
|
+
const pending = [...this.pending.values()];
|
|
101043
|
+
this.pending.clear();
|
|
101044
|
+
this.ended.clear();
|
|
101045
|
+
for (const p of pending) {
|
|
101046
|
+
p.receipt.cancelled = true;
|
|
101047
|
+
p.observer?.fail(error51);
|
|
101048
|
+
p.reject(error51);
|
|
101049
|
+
}
|
|
101050
|
+
return turns;
|
|
101051
|
+
}
|
|
101052
|
+
finish(id2) {
|
|
101053
|
+
const p = this.pending.get(id2);
|
|
101054
|
+
if (!p) return;
|
|
101055
|
+
this.pending.delete(id2);
|
|
101056
|
+
if (!this.pending.size) this.ended.clear();
|
|
101057
|
+
p.receipt.completed = true;
|
|
101058
|
+
p.observer?.complete();
|
|
101059
|
+
p.resolve();
|
|
101060
|
+
}
|
|
101061
|
+
};
|
|
101062
|
+
|
|
101063
|
+
// packages/agent-host/src/adapters/claude/index.ts
|
|
100742
101064
|
import { execFile as execFile4 } from "node:child_process";
|
|
100743
101065
|
import { promisify as promisify4 } from "node:util";
|
|
100744
101066
|
|
|
@@ -100802,7 +101124,7 @@ async function installedCliVersion(name, packageName, deps = realDeps2()) {
|
|
|
100802
101124
|
// packages/agent-host/src/adapters/claude/exe-link.ts
|
|
100803
101125
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
100804
101126
|
import { closeSync as closeSync6, copyFileSync as copyFileSync2, linkSync, mkdirSync as mkdirSync10, openSync as openSync6, readdirSync as readdirSync7, readFileSync as readFileSync10, readSync as readSync4, renameSync as renameSync10, rmSync as rmSync6, statSync as statSync11 } from "node:fs";
|
|
100805
|
-
import { basename as basename4, dirname as dirname8, join as
|
|
101127
|
+
import { basename as basename4, dirname as dirname8, join as join22 } from "node:path";
|
|
100806
101128
|
var PLACEHOLDER_MAX_BYTES = 1e6;
|
|
100807
101129
|
var ClaudePlaceholderError = class extends Error {
|
|
100808
101130
|
name = "ClaudePlaceholderError";
|
|
@@ -100838,7 +101160,7 @@ function placeholderProblem(exe, size, fs2 = realExeFs) {
|
|
|
100838
101160
|
}
|
|
100839
101161
|
if (head.toString("latin1") === "MZ") return null;
|
|
100840
101162
|
const pkg = dirname8(dirname8(exe));
|
|
100841
|
-
const installer = basename4(dirname8(exe)).toLowerCase() === "bin" ?
|
|
101163
|
+
const installer = basename4(dirname8(exe)).toLowerCase() === "bin" ? join22(pkg, "install.cjs") : null;
|
|
100842
101164
|
let canRerun = false;
|
|
100843
101165
|
if (installer) {
|
|
100844
101166
|
try {
|
|
@@ -100853,14 +101175,14 @@ function placeholderProblem(exe, size, fs2 = realExeFs) {
|
|
|
100853
101175
|
canRerun ? `To fix it, close every Claude Code (Centralu's Claude sessions, terminals, editors) and run \`node "${installer}"\`, or reinstall with \`npm i -g @anthropic-ai/claude-code\` while no Claude Code is running.` : "To fix it, close every Claude Code (Centralu's Claude sessions, terminals, editors) and reinstall it with `npm i -g @anthropic-ai/claude-code`."
|
|
100854
101176
|
].join(" ");
|
|
100855
101177
|
}
|
|
100856
|
-
function claudeKey(exe,
|
|
101178
|
+
function claudeKey(exe, stat7, fs2 = realExeFs) {
|
|
100857
101179
|
let version2 = null;
|
|
100858
101180
|
try {
|
|
100859
|
-
const pkg = JSON.parse(fs2.read(
|
|
101181
|
+
const pkg = JSON.parse(fs2.read(join22(dirname8(dirname8(exe)), "package.json")));
|
|
100860
101182
|
if (pkg.name === "@anthropic-ai/claude-code" && typeof pkg.version === "string" && pkg.version) version2 = pkg.version;
|
|
100861
101183
|
} catch {
|
|
100862
101184
|
}
|
|
100863
|
-
const raw = version2 ? `${version2}-${
|
|
101185
|
+
const raw = version2 ? `${version2}-${stat7.size}` : `${stat7.size}-${Math.floor(stat7.mtimeMs)}`;
|
|
100864
101186
|
return raw.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
100865
101187
|
}
|
|
100866
101188
|
function windowsStart(platform = process.platform) {
|
|
@@ -100882,7 +101204,7 @@ var ClaudeLinks = class {
|
|
|
100882
101204
|
return windowsStart(this.opts.platform);
|
|
100883
101205
|
}
|
|
100884
101206
|
base() {
|
|
100885
|
-
return
|
|
101207
|
+
return join22(this.opts.root(), "tools", "claude");
|
|
100886
101208
|
}
|
|
100887
101209
|
/**
|
|
100888
101210
|
* What to start for the `claude.exe` found at `exe`. Off Windows, or for anything but an `.exe`
|
|
@@ -100893,26 +101215,26 @@ var ClaudeLinks = class {
|
|
|
100893
101215
|
prepare(exe) {
|
|
100894
101216
|
const direct = { path: exe, key: null };
|
|
100895
101217
|
if (!this.enabled || !/\.exe$/i.test(exe)) return direct;
|
|
100896
|
-
let
|
|
101218
|
+
let stat7;
|
|
100897
101219
|
try {
|
|
100898
|
-
|
|
101220
|
+
stat7 = this.fs.stat(exe);
|
|
100899
101221
|
} catch {
|
|
100900
101222
|
return direct;
|
|
100901
101223
|
}
|
|
100902
|
-
const problem2 = placeholderProblem(exe,
|
|
101224
|
+
const problem2 = placeholderProblem(exe, stat7.size, this.fs);
|
|
100903
101225
|
if (problem2) throw new ClaudePlaceholderError(problem2);
|
|
100904
|
-
const key = claudeKey(exe,
|
|
100905
|
-
const dir =
|
|
100906
|
-
const target =
|
|
101226
|
+
const key = claudeKey(exe, stat7, this.fs);
|
|
101227
|
+
const dir = join22(this.base(), key);
|
|
101228
|
+
const target = join22(dir, "claude.exe");
|
|
100907
101229
|
const ready = () => {
|
|
100908
101230
|
try {
|
|
100909
|
-
return this.fs.stat(target).size ===
|
|
101231
|
+
return this.fs.stat(target).size === stat7.size;
|
|
100910
101232
|
} catch {
|
|
100911
101233
|
return false;
|
|
100912
101234
|
}
|
|
100913
101235
|
};
|
|
100914
101236
|
if (ready()) return this.use(key, target);
|
|
100915
|
-
const tmp =
|
|
101237
|
+
const tmp = join22(dir, `claude-${randomBytes2(4).toString("hex")}.tmp`);
|
|
100916
101238
|
try {
|
|
100917
101239
|
this.fs.mkdir(dir);
|
|
100918
101240
|
try {
|
|
@@ -100972,7 +101294,7 @@ var ClaudeLinks = class {
|
|
|
100972
101294
|
for (const name of names) {
|
|
100973
101295
|
if (name === this.current || this.uses.has(name)) continue;
|
|
100974
101296
|
try {
|
|
100975
|
-
this.fs.removeDir(
|
|
101297
|
+
this.fs.removeDir(join22(this.base(), name));
|
|
100976
101298
|
} catch {
|
|
100977
101299
|
}
|
|
100978
101300
|
}
|
|
@@ -102426,7 +102748,14 @@ function alwaysAllowKey(detail, opts = {}) {
|
|
|
102426
102748
|
}
|
|
102427
102749
|
|
|
102428
102750
|
// packages/agent-host/src/adapters/claude/cross-session.ts
|
|
102429
|
-
function withoutCrossSessionMessaging(options) {
|
|
102751
|
+
function withoutCrossSessionMessaging(options, preset = "normal") {
|
|
102752
|
+
if (preset === "auto")
|
|
102753
|
+
return {
|
|
102754
|
+
...options,
|
|
102755
|
+
settings: options.settings ?? {},
|
|
102756
|
+
disallowedTools: options.disallowedTools ?? [],
|
|
102757
|
+
env: { ...process.env, ...options.env }
|
|
102758
|
+
};
|
|
102430
102759
|
const env = {
|
|
102431
102760
|
...process.env,
|
|
102432
102761
|
...options.env,
|
|
@@ -102526,7 +102855,7 @@ var ClaudeSession = class {
|
|
|
102526
102855
|
this.opts = opts;
|
|
102527
102856
|
this.emit = emit;
|
|
102528
102857
|
this.launch = launch2;
|
|
102529
|
-
this.ended = new Promise((
|
|
102858
|
+
this.ended = new Promise((resolve12) => this.markEnded = resolve12);
|
|
102530
102859
|
this.stream = new ClaudeStreamNormalizer(sessionId, () => ({
|
|
102531
102860
|
model: opts.model ?? null,
|
|
102532
102861
|
effort: opts.effort ?? null,
|
|
@@ -102543,6 +102872,12 @@ var ClaudeSession = class {
|
|
|
102543
102872
|
launch;
|
|
102544
102873
|
externalId = null;
|
|
102545
102874
|
queue = [];
|
|
102875
|
+
inputReceipts = new InputReceipts();
|
|
102876
|
+
/** Person messages already handed to the SDK, so an interrupt receipt can report cancellations. */
|
|
102877
|
+
personInputs = /* @__PURE__ */ new Set();
|
|
102878
|
+
trackedInputKnown = false;
|
|
102879
|
+
/** Installed only after the initial CLI metadata confirms the protocol we have verified. */
|
|
102880
|
+
sendTracked;
|
|
102546
102881
|
/** The CLI version this process last reported (#297) */
|
|
102547
102882
|
reportedVersion = null;
|
|
102548
102883
|
notify = null;
|
|
@@ -102635,11 +102970,13 @@ var ClaudeSession = class {
|
|
|
102635
102970
|
const next = self2.queue.shift();
|
|
102636
102971
|
if (next !== void 0) {
|
|
102637
102972
|
self2.inTurn.push(next);
|
|
102973
|
+
if (!next.parentNote) self2.personInputs.add(next.uuid);
|
|
102638
102974
|
yield {
|
|
102639
102975
|
type: "user",
|
|
102640
102976
|
parent_tool_use_id: null,
|
|
102641
102977
|
session_id: self2.externalId ?? "",
|
|
102642
|
-
|
|
102978
|
+
uuid: next.uuid,
|
|
102979
|
+
message: { role: "user", content: [{ type: "text", text: next.text }] }
|
|
102643
102980
|
};
|
|
102644
102981
|
continue;
|
|
102645
102982
|
}
|
|
@@ -102769,8 +103106,8 @@ var ClaudeSession = class {
|
|
|
102769
103106
|
if (questions.length === 0) return { behavior: "allow", updatedInput: toolInput };
|
|
102770
103107
|
const requestId2 = `q-${++self2.reqCounter}`;
|
|
102771
103108
|
self2.emit({ type: "question_request", sessionId: self2.sessionId, requestId: requestId2, questions });
|
|
102772
|
-
return new Promise((
|
|
102773
|
-
self2.questions.set(requestId2, { resolve:
|
|
103109
|
+
return new Promise((resolve12) => {
|
|
103110
|
+
self2.questions.set(requestId2, { resolve: resolve12, input: toolInput });
|
|
102774
103111
|
self2.releaseOnCancel(requestId2, options?.signal);
|
|
102775
103112
|
});
|
|
102776
103113
|
}
|
|
@@ -102779,17 +103116,17 @@ var ClaudeSession = class {
|
|
|
102779
103116
|
if (key && self2.alwaysAllow.allows(key)) return { behavior: "allow", updatedInput: toolInput };
|
|
102780
103117
|
const requestId = `req-${++self2.reqCounter}`;
|
|
102781
103118
|
self2.emit({ type: "approval_request", sessionId: self2.sessionId, requestId, detail });
|
|
102782
|
-
return new Promise((
|
|
102783
|
-
self2.pending.set(requestId, { resolve:
|
|
103119
|
+
return new Promise((resolve12) => {
|
|
103120
|
+
self2.pending.set(requestId, { resolve: resolve12, input: toolInput });
|
|
102784
103121
|
self2.releaseOnCancel(requestId, options?.signal);
|
|
102785
103122
|
});
|
|
102786
103123
|
}
|
|
102787
|
-
})
|
|
103124
|
+
}, preset)
|
|
102788
103125
|
});
|
|
102789
103126
|
ClaudeAdapter.liveQueries.add(q2);
|
|
102790
103127
|
ClaudeAdapter.querySessions.set(q2, this.sessionId);
|
|
102791
103128
|
this.releaseLostCalls(q2);
|
|
102792
|
-
this.goalKnown = new Promise((
|
|
103129
|
+
this.goalKnown = new Promise((resolve12) => this.settleGoalKnown = resolve12);
|
|
102793
103130
|
q2.supportedCommands().then((list) => {
|
|
102794
103131
|
if (this.goalCommand === void 0 && Array.isArray(list)) this.goalCommand = list.some((c) => c?.name === "goal");
|
|
102795
103132
|
}).catch(() => {
|
|
@@ -102800,7 +103137,23 @@ var ClaudeSession = class {
|
|
|
102800
103137
|
for await (const msg of q2) {
|
|
102801
103138
|
if (this.closed) break;
|
|
102802
103139
|
const m = msg;
|
|
103140
|
+
if (!this.turnOpen && m.user_message_uuid && !m.parent_tool_use_id && (m.type === "assistant" || m.type === "stream_event")) {
|
|
103141
|
+
this.turnOpen = true;
|
|
103142
|
+
if (!this.pending.size && !this.questions.size) this.emit({ type: "state_change", sessionId: this.sessionId, state: "working" });
|
|
103143
|
+
}
|
|
102803
103144
|
if (m.type === "system" && m.subtype === "init" && m.session_id) this.externalId = m.session_id;
|
|
103145
|
+
if (m.type === "system" && m.subtype === "init" && !this.trackedInputKnown) {
|
|
103146
|
+
this.trackedInputKnown = true;
|
|
103147
|
+
const version2 = typeof m.claude_code_version === "string" ? /^(\d+)\.(\d+)\.(\d+)$/.exec(m.claude_code_version) : null;
|
|
103148
|
+
const supported = version2 && (Number(version2[1]) > 2 || Number(version2[1]) === 2 && (Number(version2[2]) > 1 || Number(version2[2]) === 1 && Number(version2[3]) >= 289));
|
|
103149
|
+
if (supported && Array.isArray(m.capabilities) && m.capabilities.includes("interrupt_receipt_v1") && m.capabilities.includes("interrupt_cancel_queued_v1")) {
|
|
103150
|
+
this.sendTracked = (text2, observer) => {
|
|
103151
|
+
const receipt = this.inputReceipts.create(observer);
|
|
103152
|
+
this.sendInput(text2, receipt.id, receipt);
|
|
103153
|
+
return receipt.done;
|
|
103154
|
+
};
|
|
103155
|
+
}
|
|
103156
|
+
}
|
|
102804
103157
|
if (m.type === "system" && m.subtype === "init" && typeof m.claude_code_version === "string" && m.claude_code_version !== this.reportedVersion) {
|
|
102805
103158
|
this.reportedVersion = m.claude_code_version;
|
|
102806
103159
|
this.emit({ type: "agent_version", sessionId: this.sessionId, version: m.claude_code_version });
|
|
@@ -102814,8 +103167,23 @@ var ClaudeSession = class {
|
|
|
102814
103167
|
if (m.type === "result") this.turnOpen = false;
|
|
102815
103168
|
let events2 = this.stream.push(msg);
|
|
102816
103169
|
const retrying = m.type === "result" && this.retryAfterRefreshRace(msg);
|
|
103170
|
+
if (m.type === "result" && !retrying) {
|
|
103171
|
+
const result = msg;
|
|
103172
|
+
for (const id2 of result.user_message_uuids ?? (result.user_message_uuid ? [result.user_message_uuid] : [])) this.personInputs.delete(id2);
|
|
103173
|
+
}
|
|
103174
|
+
let completedInputs;
|
|
103175
|
+
if (m.type === "result" && !retrying && this.inputReceipts.size && events2.some((e) => e.type === "turn_complete" || e.type === "error")) {
|
|
103176
|
+
const result = msg;
|
|
103177
|
+
const ids = result.user_message_uuids ?? [];
|
|
103178
|
+
const failure3 = events2.find((e) => e.type === "error");
|
|
103179
|
+
if (ids.length && failure3?.type === "error") this.cancelParentNotes(new Error(failure3.error.message));
|
|
103180
|
+
else if (ids.length) completedInputs = ids;
|
|
103181
|
+
}
|
|
102817
103182
|
if (retrying) events2 = events2.filter((e) => e.type !== "error");
|
|
102818
|
-
for (const e of events2)
|
|
103183
|
+
for (const e of events2) {
|
|
103184
|
+
this.emit(e);
|
|
103185
|
+
if (e.type === "turn_complete" && completedInputs) this.inputReceipts.complete(completedInputs);
|
|
103186
|
+
}
|
|
102819
103187
|
this.armScheduleLook();
|
|
102820
103188
|
if (m.type === "result" && !retrying) void this.reportContext(q2);
|
|
102821
103189
|
}
|
|
@@ -102984,14 +103352,18 @@ var ClaudeSession = class {
|
|
|
102984
103352
|
}
|
|
102985
103353
|
}
|
|
102986
103354
|
send(text2) {
|
|
103355
|
+
this.sendInput(text2);
|
|
103356
|
+
}
|
|
103357
|
+
sendInput(text2, uuid3 = randomUUID9(), note) {
|
|
102987
103358
|
const goal = isGoalCommand(text2);
|
|
102988
103359
|
if (!this.sendGate && !(goal && this.goalCommand === void 0)) {
|
|
102989
|
-
this.deliver(text2);
|
|
103360
|
+
this.deliver(text2, uuid3, !!note);
|
|
102990
103361
|
return;
|
|
102991
103362
|
}
|
|
102992
103363
|
const gate = this.sendGate = (this.sendGate ?? this.goalKnown).then(() => {
|
|
103364
|
+
if (note?.cancelled) return;
|
|
102993
103365
|
if (!this.closed) {
|
|
102994
|
-
this.deliver(text2);
|
|
103366
|
+
this.deliver(text2, uuid3, !!note);
|
|
102995
103367
|
return;
|
|
102996
103368
|
}
|
|
102997
103369
|
this.emit({
|
|
@@ -103008,7 +103380,7 @@ var ClaudeSession = class {
|
|
|
103008
103380
|
if (this.sendGate === gate) this.sendGate = null;
|
|
103009
103381
|
});
|
|
103010
103382
|
}
|
|
103011
|
-
deliver(text2) {
|
|
103383
|
+
deliver(text2, uuid3, parentNote) {
|
|
103012
103384
|
if (isGoalCommand(text2)) {
|
|
103013
103385
|
if (this.goalCommand !== true) {
|
|
103014
103386
|
this.emit({
|
|
@@ -103023,11 +103395,12 @@ var ClaudeSession = class {
|
|
|
103023
103395
|
}
|
|
103024
103396
|
this.stream.goal.commandSent();
|
|
103025
103397
|
}
|
|
103026
|
-
this.
|
|
103398
|
+
const starting = !this.turnOpen;
|
|
103399
|
+
this.queue.push({ text: text2, uuid: uuid3, parentNote });
|
|
103027
103400
|
this.turnOpen = true;
|
|
103028
103401
|
this.notify?.();
|
|
103029
103402
|
this.notify = null;
|
|
103030
|
-
this.emit({ type: "state_change", sessionId: this.sessionId, state: "working" });
|
|
103403
|
+
if (starting) this.emit({ type: "state_change", sessionId: this.sessionId, state: "working" });
|
|
103031
103404
|
}
|
|
103032
103405
|
respondApproval(requestId, decision, scope, matcher) {
|
|
103033
103406
|
const p = this.pending.get(requestId);
|
|
@@ -103166,7 +103539,11 @@ var ClaudeSession = class {
|
|
|
103166
103539
|
* 2) SDK interrupt: this actually cuts the turn. We can call this method because we pass the
|
|
103167
103540
|
* prompt as an async generator, in streaming input mode.
|
|
103168
103541
|
*/
|
|
103169
|
-
interrupt() {
|
|
103542
|
+
interrupt(cancelQueued = this.inputReceipts.size > 0, reason = new Error("The parent note was stopped before it finished")) {
|
|
103543
|
+
this.inputReceipts.failAll(reason);
|
|
103544
|
+
const held3 = (cancelQueued ? this.queue.filter((input) => !input.parentNote).length : 0) + (this.retry?.texts.filter((input) => !input.parentNote).length ?? 0);
|
|
103545
|
+
if (cancelQueued) this.queue.length = 0;
|
|
103546
|
+
this.reportUndelivered(held3);
|
|
103170
103547
|
this.opts.apps?.cancelAll();
|
|
103171
103548
|
this.cancelRetry();
|
|
103172
103549
|
for (const [id2, p] of this.pending) {
|
|
@@ -103176,7 +103553,13 @@ var ClaudeSession = class {
|
|
|
103176
103553
|
this.pending.clear();
|
|
103177
103554
|
this.releaseQuestions("Stopped by user");
|
|
103178
103555
|
if (this.turnOpen) this.stream.stopped();
|
|
103179
|
-
void this.query?.interrupt().
|
|
103556
|
+
void this.query?.interrupt(cancelQueued ? { cancelQueued: true } : void 0).then((response) => {
|
|
103557
|
+
const cancelled = response?.cancelled;
|
|
103558
|
+
if (!Array.isArray(cancelled)) return;
|
|
103559
|
+
let count = 0;
|
|
103560
|
+
for (const id2 of cancelled) if (typeof id2 === "string" && this.personInputs.delete(id2)) count++;
|
|
103561
|
+
this.reportUndelivered(count);
|
|
103562
|
+
}).catch((err) => {
|
|
103180
103563
|
this.emit({
|
|
103181
103564
|
type: "error",
|
|
103182
103565
|
sessionId: this.sessionId,
|
|
@@ -103185,6 +103568,34 @@ var ClaudeSession = class {
|
|
|
103185
103568
|
});
|
|
103186
103569
|
this.emit({ type: "state_change", sessionId: this.sessionId, state: "waiting_input", reason: "interrupted" });
|
|
103187
103570
|
}
|
|
103571
|
+
/** A failed turn is already over. Cancel its parent notes without interrupting the person's next turn. */
|
|
103572
|
+
cancelParentNotes(reason) {
|
|
103573
|
+
const ids = this.inputReceipts.ids;
|
|
103574
|
+
this.inputReceipts.failAll(reason);
|
|
103575
|
+
this.queue = this.queue.filter((input) => !input.parentNote);
|
|
103576
|
+
if (this.retry) {
|
|
103577
|
+
const texts = this.retry.texts;
|
|
103578
|
+
texts.splice(0, texts.length, ...texts.filter((input) => !input.parentNote));
|
|
103579
|
+
if (!texts.length) this.cancelRetry();
|
|
103580
|
+
}
|
|
103581
|
+
const query = this.query;
|
|
103582
|
+
if (!query?.cancelAsyncMessage || this.personInputs.size || this.queue.length || this.retry?.texts.length || this.sendGate) return;
|
|
103583
|
+
for (const id2 of ids) void query.cancelAsyncMessage(id2).catch((error51) => {
|
|
103584
|
+
this.emit({ type: "error", sessionId: this.sessionId, error: {
|
|
103585
|
+
code: "internal",
|
|
103586
|
+
message: `Could not cancel the parent note: ${error51.message}`,
|
|
103587
|
+
retryable: false
|
|
103588
|
+
} });
|
|
103589
|
+
});
|
|
103590
|
+
}
|
|
103591
|
+
reportUndelivered(count) {
|
|
103592
|
+
if (!count) return;
|
|
103593
|
+
this.emit({
|
|
103594
|
+
type: "error",
|
|
103595
|
+
sessionId: this.sessionId,
|
|
103596
|
+
error: { code: "internal", message: `${count} message(s) were still queued when the turn stopped and were not delivered \u2014 please resend`, retryable: false }
|
|
103597
|
+
});
|
|
103598
|
+
}
|
|
103188
103599
|
/**
|
|
103189
103600
|
* Stops one background task (#290). Measured on a backgrounded shell: `background_tasks_changed` without it,
|
|
103190
103601
|
* `task_updated` killed and `task_notification` stopped follow at once, and the stream turns that into the
|
|
@@ -103203,6 +103614,7 @@ var ClaudeSession = class {
|
|
|
103203
103614
|
* same way; only swapping the process was missing it.
|
|
103204
103615
|
*/
|
|
103205
103616
|
async dispose() {
|
|
103617
|
+
this.inputReceipts.failAll(new Error("The session closed before the parent note finished"));
|
|
103206
103618
|
this.closed = true;
|
|
103207
103619
|
this.notify?.();
|
|
103208
103620
|
this.settleGoalKnown();
|
|
@@ -103244,6 +103656,7 @@ var ClaudeSession = class {
|
|
|
103244
103656
|
* the process (#98) — and ends every background task the session listed (#290), for the same reason.
|
|
103245
103657
|
*/
|
|
103246
103658
|
releaseAgents(why) {
|
|
103659
|
+
this.inputReceipts.failAll(new Error(why));
|
|
103247
103660
|
if (this.scheduleLook) clearTimeout(this.scheduleLook);
|
|
103248
103661
|
this.scheduleLook = null;
|
|
103249
103662
|
for (const e of this.stream.release(why)) this.emit(e);
|
|
@@ -103391,6 +103804,8 @@ var ClaudeAdapter = class _ClaudeAdapter {
|
|
|
103391
103804
|
processModel: "per-session",
|
|
103392
103805
|
builtInSubagents: true,
|
|
103393
103806
|
peerMessaging: true,
|
|
103807
|
+
// The query's prompt iterator reads queued user messages while the current turn continues.
|
|
103808
|
+
midTurnInput: true,
|
|
103394
103809
|
/*
|
|
103395
103810
|
* An idle session may be put to sleep (#568): a resumed process read the prompt cache as the live one did (~100% at
|
|
103396
103811
|
* 2, 6 and 10 min on the 1 h lifetime, CLI 2.1.29x with haiku, #561) for +0.7 s (p50) / +1.8 s (p95) before the first
|
|
@@ -103400,7 +103815,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
|
|
|
103400
103815
|
sleep: true
|
|
103401
103816
|
};
|
|
103402
103817
|
launchSettingsFingerprint(settings) {
|
|
103403
|
-
return JSON.stringify({ v: 1, cwd: settings.cwd, permission: settings.permissionPreset, trusted: !!settings.projectTrusted, noSettings: !!settings.noSettingFiles });
|
|
103818
|
+
return JSON.stringify({ v: settings.permissionPreset === "auto" ? 2 : 1, cwd: settings.cwd, permission: settings.permissionPreset, trusted: !!settings.projectTrusted, noSettings: !!settings.noSettingFiles });
|
|
103404
103819
|
}
|
|
103405
103820
|
async detect() {
|
|
103406
103821
|
const path2 = whichTool("claude");
|
|
@@ -103487,7 +103902,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
|
|
|
103487
103902
|
let timer;
|
|
103488
103903
|
await Promise.race([
|
|
103489
103904
|
Promise.allSettled([...this.launch.closing]),
|
|
103490
|
-
new Promise((
|
|
103905
|
+
new Promise((resolve12) => timer = setTimeout(resolve12, this.launch.exitGraceMs))
|
|
103491
103906
|
]);
|
|
103492
103907
|
clearTimeout(timer);
|
|
103493
103908
|
}
|
|
@@ -103497,7 +103912,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
|
|
|
103497
103912
|
import { execFile as execFile5 } from "node:child_process";
|
|
103498
103913
|
|
|
103499
103914
|
// packages/agent-host/src/adapters/codex/bridge-path.ts
|
|
103500
|
-
import { existsSync as
|
|
103915
|
+
import { existsSync as existsSync15 } from "node:fs";
|
|
103501
103916
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
103502
103917
|
function bridgePath() {
|
|
103503
103918
|
const candidates = [
|
|
@@ -103506,23 +103921,23 @@ function bridgePath() {
|
|
|
103506
103921
|
new URL("./codex-orchestrator-bridge.mjs", import.meta.url)
|
|
103507
103922
|
// the bundled build-output layout
|
|
103508
103923
|
].map((u) => fileURLToPath3(u));
|
|
103509
|
-
const found = candidates.find((p) =>
|
|
103924
|
+
const found = candidates.find((p) => existsSync15(p));
|
|
103510
103925
|
if (!found) throw new Error(`orchestrator bridge not found: ${candidates.join(", ")}`);
|
|
103511
103926
|
return found;
|
|
103512
103927
|
}
|
|
103513
103928
|
|
|
103514
103929
|
// packages/agent-host/src/adapters/codex/index.ts
|
|
103515
|
-
import { existsSync as
|
|
103930
|
+
import { existsSync as existsSync17, realpathSync as realpathSync7 } from "node:fs";
|
|
103516
103931
|
import { homedir as homedir10 } from "node:os";
|
|
103517
|
-
import { dirname as dirname9, join as
|
|
103932
|
+
import { dirname as dirname9, join as join24, resolve as resolve9 } from "node:path";
|
|
103518
103933
|
import { promisify as promisify5 } from "node:util";
|
|
103519
103934
|
|
|
103520
103935
|
// packages/agent-host/src/adapters/codex/native.ts
|
|
103521
|
-
import { existsSync as
|
|
103936
|
+
import { existsSync as existsSync16, readFileSync as readFileSync11, realpathSync as realpathSync6 } from "node:fs";
|
|
103522
103937
|
var realDeps3 = () => ({
|
|
103523
103938
|
platform: HOST_PLATFORM,
|
|
103524
103939
|
arch: process.arch,
|
|
103525
|
-
exists:
|
|
103940
|
+
exists: existsSync16,
|
|
103526
103941
|
read: (p) => readFileSync11(p, "utf8"),
|
|
103527
103942
|
realpath: (p) => realpathSync6(p),
|
|
103528
103943
|
launch: (p) => launchFor(p),
|
|
@@ -103691,14 +104106,14 @@ var CodexClient = class {
|
|
|
103691
104106
|
if (this.closed) return Promise.reject(new Error("codex app-server has already exited"));
|
|
103692
104107
|
const id2 = `${this.idPrefix}${this.nextId++}`;
|
|
103693
104108
|
this.write({ id: id2, method, params });
|
|
103694
|
-
return new Promise((
|
|
104109
|
+
return new Promise((resolve12, reject) => {
|
|
103695
104110
|
const timer = setTimeout(() => {
|
|
103696
104111
|
if (this.pending.delete(id2)) reject(new Error(`No response for ${method}`));
|
|
103697
104112
|
}, timeoutMs);
|
|
103698
104113
|
this.pending.set(id2, {
|
|
103699
104114
|
resolve: (v) => {
|
|
103700
104115
|
clearTimeout(timer);
|
|
103701
|
-
|
|
104116
|
+
resolve12(v);
|
|
103702
104117
|
},
|
|
103703
104118
|
reject: (e) => {
|
|
103704
104119
|
clearTimeout(timer);
|
|
@@ -103722,12 +104137,12 @@ var CodexClient = class {
|
|
|
103722
104137
|
if (this.closed) return;
|
|
103723
104138
|
this.closed = true;
|
|
103724
104139
|
this.proc.stdin.end();
|
|
103725
|
-
const exited = await new Promise((
|
|
103726
|
-
if (this.proc.exitCode !== null || this.proc.signalCode != null || this.finished) return
|
|
103727
|
-
const t = setTimeout(() =>
|
|
104140
|
+
const exited = await new Promise((resolve12) => {
|
|
104141
|
+
if (this.proc.exitCode !== null || this.proc.signalCode != null || this.finished) return resolve12(true);
|
|
104142
|
+
const t = setTimeout(() => resolve12(false), 2e3);
|
|
103728
104143
|
this.proc.once("exit", () => {
|
|
103729
104144
|
clearTimeout(t);
|
|
103730
|
-
|
|
104145
|
+
resolve12(true);
|
|
103731
104146
|
});
|
|
103732
104147
|
});
|
|
103733
104148
|
if (!exited) this.proc.kill("SIGKILL");
|
|
@@ -103749,18 +104164,18 @@ var CodexClient = class {
|
|
|
103749
104164
|
|
|
103750
104165
|
// packages/agent-host/src/adapters/codex/rollout.ts
|
|
103751
104166
|
import { createReadStream as createReadStream2 } from "node:fs";
|
|
103752
|
-
import { readdir as
|
|
104167
|
+
import { readdir as readdir5 } from "node:fs/promises";
|
|
103753
104168
|
import { createInterface as createInterface2 } from "node:readline";
|
|
103754
104169
|
import { homedir as homedir6 } from "node:os";
|
|
103755
|
-
import { join as
|
|
104170
|
+
import { join as join23 } from "node:path";
|
|
103756
104171
|
var MIN_SUMMARY_CHARS = 200;
|
|
103757
|
-
async function findRolloutPath(threadId, sessionsDir =
|
|
104172
|
+
async function findRolloutPath(threadId, sessionsDir = join23(homedir6(), ".codex", "sessions")) {
|
|
103758
104173
|
try {
|
|
103759
104174
|
const suffix = `-${threadId}.jsonl`;
|
|
103760
|
-
const names = await
|
|
104175
|
+
const names = await readdir5(sessionsDir, { recursive: true });
|
|
103761
104176
|
const hits = names.filter((n) => String(n).endsWith(suffix)).sort();
|
|
103762
104177
|
const hit = hits[hits.length - 1];
|
|
103763
|
-
return hit ?
|
|
104178
|
+
return hit ? join23(sessionsDir, String(hit)) : null;
|
|
103764
104179
|
} catch {
|
|
103765
104180
|
return null;
|
|
103766
104181
|
}
|
|
@@ -103961,7 +104376,7 @@ function userInputText(content) {
|
|
|
103961
104376
|
}
|
|
103962
104377
|
|
|
103963
104378
|
// packages/agent-host/src/adapters/codex/images.ts
|
|
103964
|
-
import { readFile as
|
|
104379
|
+
import { readFile as readFile3, stat as stat5 } from "node:fs/promises";
|
|
103965
104380
|
var IMAGE_MIMES2 = {
|
|
103966
104381
|
png: "image/png",
|
|
103967
104382
|
jpg: "image/jpeg",
|
|
@@ -103977,9 +104392,9 @@ async function imageEventFromDisk(sessionId, path2, maxBytes = IMAGE_MAX_BYTES)
|
|
|
103977
104392
|
const mime = IMAGE_MIMES2[ext2];
|
|
103978
104393
|
if (!mime) return fail4(`Cannot display this format (.${ext2})`);
|
|
103979
104394
|
try {
|
|
103980
|
-
const s = await
|
|
104395
|
+
const s = await stat5(path2);
|
|
103981
104396
|
if (s.size > maxBytes) return fail4(`Image is too large (${Math.round(s.size / 1048576)}MB)`);
|
|
103982
|
-
const buf = await
|
|
104397
|
+
const buf = await readFile3(path2);
|
|
103983
104398
|
return { type: "message_image", sessionId, mime, data: buf.toString("base64"), path: path2 };
|
|
103984
104399
|
} catch (err) {
|
|
103985
104400
|
return fail4(`Failed to read image: ${err.message}`);
|
|
@@ -104963,16 +105378,16 @@ var CodexChildTracker = class {
|
|
|
104963
105378
|
};
|
|
104964
105379
|
|
|
104965
105380
|
// packages/agent-host/src/adapters/codex/sandbox.ts
|
|
104966
|
-
import { realpath as
|
|
104967
|
-
import { resolve as
|
|
105381
|
+
import { realpath as realpath6 } from "node:fs/promises";
|
|
105382
|
+
import { resolve as resolve8 } from "node:path";
|
|
104968
105383
|
async function sandboxConfig(cwd, preset, readConfig) {
|
|
104969
105384
|
const roots = [];
|
|
104970
105385
|
try {
|
|
104971
105386
|
const stdout = await runGit(cwd, ["rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"], { timeout: 5e3 });
|
|
104972
105387
|
const [gitDir, commonDir] = stdout.trim().split(/\r?\n/);
|
|
104973
105388
|
if (gitDir && commonDir) {
|
|
104974
|
-
const gitPath = await
|
|
104975
|
-
const commonPath = await
|
|
105389
|
+
const gitPath = await realpath6(resolve8(cwd, gitDir));
|
|
105390
|
+
const commonPath = await realpath6(resolve8(cwd, commonDir));
|
|
104976
105391
|
if (gitPath !== commonPath) roots.push(commonPath);
|
|
104977
105392
|
}
|
|
104978
105393
|
} catch {
|
|
@@ -105000,7 +105415,7 @@ function permissionOptionsFor2(preset) {
|
|
|
105000
105415
|
function repoFilesConfig(opts) {
|
|
105001
105416
|
if (opts.projectTrusted === true && !opts.noSettingFiles) return {};
|
|
105002
105417
|
const keys = /* @__PURE__ */ new Set();
|
|
105003
|
-
for (const start2 of [
|
|
105418
|
+
for (const start2 of [resolve9(opts.cwd), realPathOr(opts.cwd)]) {
|
|
105004
105419
|
for (let dir = start2; ; dir = dirname9(dir)) {
|
|
105005
105420
|
keys.add(dir);
|
|
105006
105421
|
if (dirname9(dir) === dir) break;
|
|
@@ -105015,7 +105430,7 @@ function realPathOr(path2) {
|
|
|
105015
105430
|
try {
|
|
105016
105431
|
return realpathSync7.native(path2);
|
|
105017
105432
|
} catch {
|
|
105018
|
-
return
|
|
105433
|
+
return resolve9(path2);
|
|
105019
105434
|
}
|
|
105020
105435
|
}
|
|
105021
105436
|
var APP_APPROVAL_MODE = {
|
|
@@ -105157,6 +105572,8 @@ var CodexSession = class {
|
|
|
105157
105572
|
* **that we ourselves started**, and flush the queue as a single turn once that turn ends.
|
|
105158
105573
|
*/
|
|
105159
105574
|
pendingInputs = [];
|
|
105575
|
+
inputReceipts = new InputReceipts();
|
|
105576
|
+
inputGroups = /* @__PURE__ */ new Map();
|
|
105160
105577
|
/** An unsteerable turn (compact/review) is running — we know because we are the ones who started it */
|
|
105161
105578
|
blockingTurn = false;
|
|
105162
105579
|
/**
|
|
@@ -105524,6 +105941,16 @@ var CodexSession = class {
|
|
|
105524
105941
|
this.activityBefore = null;
|
|
105525
105942
|
}
|
|
105526
105943
|
this.noteAppCall(n);
|
|
105944
|
+
if (n.method === "item/started" || n.method === "item/completed") {
|
|
105945
|
+
const p = n.params ?? {};
|
|
105946
|
+
if (p.item?.type === "userMessage" && p.item.clientId && p.turnId) {
|
|
105947
|
+
const group = this.inputGroups.get(p.item.clientId);
|
|
105948
|
+
if (group) {
|
|
105949
|
+
this.inputGroups.delete(p.item.clientId);
|
|
105950
|
+
this.takeInputTurn({ turn: { id: p.turnId } }, group);
|
|
105951
|
+
}
|
|
105952
|
+
}
|
|
105953
|
+
}
|
|
105527
105954
|
if (n.method === "turn/completed" && this.blockingTurn) {
|
|
105528
105955
|
this.blockingTurn = false;
|
|
105529
105956
|
this.flushPending();
|
|
@@ -105541,6 +105968,7 @@ var CodexSession = class {
|
|
|
105541
105968
|
}
|
|
105542
105969
|
if (n.method === "mcpServer/startupStatus/updated" && !this.firstMcpFailure(n.params)) return;
|
|
105543
105970
|
for (const e of normalizeNotification(this.sessionId, n)) {
|
|
105971
|
+
if (e.type === "error" && this.inputReceipts.size) this.interrupt(new Error(e.error.message));
|
|
105544
105972
|
if (e.type === "message_image" && !e.data && e.path) {
|
|
105545
105973
|
void imageEventFromDisk(this.sessionId, e.path).then((filled) => this.emit(filled));
|
|
105546
105974
|
continue;
|
|
@@ -105559,6 +105987,10 @@ var CodexSession = class {
|
|
|
105559
105987
|
else this.activityBefore = e.activity;
|
|
105560
105988
|
}
|
|
105561
105989
|
this.emit(e);
|
|
105990
|
+
if (e.type === "turn_complete") {
|
|
105991
|
+
const ended = turnIdOf(n.params);
|
|
105992
|
+
if (ended) this.inputReceipts.endTurn(ended);
|
|
105993
|
+
}
|
|
105562
105994
|
}
|
|
105563
105995
|
this.noteSpawn(n);
|
|
105564
105996
|
if (n.method === "turn/completed" && this.unlinked.size > 0) {
|
|
@@ -105730,13 +106162,21 @@ var CodexSession = class {
|
|
|
105730
106162
|
applyRules(matchers) {
|
|
105731
106163
|
this.alwaysAllow.addAll(matchers);
|
|
105732
106164
|
}
|
|
106165
|
+
sendTracked(text2, observer) {
|
|
106166
|
+
const receipt = this.inputReceipts.create(observer);
|
|
106167
|
+
this.sendInput(text2, receipt);
|
|
106168
|
+
return receipt.done;
|
|
106169
|
+
}
|
|
105733
106170
|
send(text2) {
|
|
106171
|
+
this.sendInput(text2);
|
|
106172
|
+
}
|
|
106173
|
+
sendInput(text2, receipt) {
|
|
105734
106174
|
const nth = ++this.sendsIssued;
|
|
105735
106175
|
void this.ready.then(() => {
|
|
105736
|
-
if (nth <= this.sendsStopped) return;
|
|
106176
|
+
if (nth <= this.sendsStopped || receipt?.cancelled) return;
|
|
105737
106177
|
if (!this.threadId) throw new Error("Thread is not ready");
|
|
105738
106178
|
if (this.blockingTurn) {
|
|
105739
|
-
this.pendingInputs.push(text2);
|
|
106179
|
+
this.pendingInputs.push({ text: text2, receipt });
|
|
105740
106180
|
return;
|
|
105741
106181
|
}
|
|
105742
106182
|
if (text2.trim() === "/compact") {
|
|
@@ -105789,8 +106229,10 @@ var CodexSession = class {
|
|
|
105789
106229
|
say2(status === "active" ? `Goal set: ${arg}` : `Goal set (${status}): ${arg}`);
|
|
105790
106230
|
});
|
|
105791
106231
|
}
|
|
106232
|
+
if (receipt) this.inputGroups.set(receipt.id, [receipt]);
|
|
105792
106233
|
return this.client.request("turn/start", {
|
|
105793
106234
|
threadId: this.threadId,
|
|
106235
|
+
...receipt ? { clientUserMessageId: receipt.id } : {},
|
|
105794
106236
|
input: [{ type: "text", text: text2 }],
|
|
105795
106237
|
/*
|
|
105796
106238
|
* Reasoning effort is passed per turn — this is the slot codex documents as applying
|
|
@@ -105799,8 +106241,11 @@ var CodexSession = class {
|
|
|
105799
106241
|
*/
|
|
105800
106242
|
...this.opts.effort ? { effort: this.opts.effort } : {},
|
|
105801
106243
|
...this.outputSchemaParam()
|
|
105802
|
-
}).then((res) => this.
|
|
106244
|
+
}).then((res) => this.takeInputTurn(res, receipt ? [receipt] : []));
|
|
105803
106245
|
}).catch((e) => {
|
|
106246
|
+
if (receipt) this.inputGroups.delete(receipt.id);
|
|
106247
|
+
if (receipt?.cancelled || receipt?.completed) return;
|
|
106248
|
+
if (receipt) this.interrupt(e);
|
|
105804
106249
|
if (this.startFailed) return;
|
|
105805
106250
|
this.emit({
|
|
105806
106251
|
type: "error",
|
|
@@ -105812,14 +106257,22 @@ var CodexSession = class {
|
|
|
105812
106257
|
/** Sends the messages that were queued out as one turn — each message as its own input item (does not blur the boundary) */
|
|
105813
106258
|
flushPending() {
|
|
105814
106259
|
if (this.pendingInputs.length === 0 || !this.threadId) return;
|
|
105815
|
-
const
|
|
106260
|
+
const pending = this.pendingInputs;
|
|
106261
|
+
const receipts = pending.flatMap((p) => p.receipt ? [p.receipt] : []);
|
|
106262
|
+
const clientId = receipts[0]?.id;
|
|
106263
|
+
if (clientId) this.inputGroups.set(clientId, receipts);
|
|
106264
|
+
const input = pending.map(({ text: text2 }) => ({ type: "text", text: text2 }));
|
|
105816
106265
|
this.pendingInputs = [];
|
|
105817
106266
|
void this.client.request("turn/start", {
|
|
105818
106267
|
threadId: this.threadId,
|
|
106268
|
+
...clientId ? { clientUserMessageId: clientId } : {},
|
|
105819
106269
|
input,
|
|
105820
106270
|
...this.opts.effort ? { effort: this.opts.effort } : {},
|
|
105821
106271
|
...this.outputSchemaParam()
|
|
105822
|
-
}).then((res) => this.
|
|
106272
|
+
}).then((res) => this.takeInputTurn(res, receipts)).catch((e) => {
|
|
106273
|
+
if (clientId) this.inputGroups.delete(clientId);
|
|
106274
|
+
if (receipts.length && receipts.every((r) => r.cancelled || r.completed)) return;
|
|
106275
|
+
if (this.inputReceipts.size) this.interrupt(e);
|
|
105823
106276
|
this.emit({
|
|
105824
106277
|
type: "error",
|
|
105825
106278
|
sessionId: this.sessionId,
|
|
@@ -105827,6 +106280,25 @@ var CodexSession = class {
|
|
|
105827
106280
|
});
|
|
105828
106281
|
});
|
|
105829
106282
|
}
|
|
106283
|
+
takeInputTurn(res, receipts) {
|
|
106284
|
+
if (receipts.length && receipts.every((r) => r.completed)) return;
|
|
106285
|
+
const id2 = turnIdOf(res);
|
|
106286
|
+
if (!receipts.length) {
|
|
106287
|
+
this.takeStartedTurn(id2);
|
|
106288
|
+
return;
|
|
106289
|
+
}
|
|
106290
|
+
if (!id2) {
|
|
106291
|
+
this.interrupt(new Error("Codex did not identify the turn accepting the parent note"));
|
|
106292
|
+
return;
|
|
106293
|
+
}
|
|
106294
|
+
if (receipts.some((r) => r.cancelled) && !this.endedTurns.includes(id2)) {
|
|
106295
|
+
void this.client.request("turn/interrupt", { threadId: this.threadId, turnId: id2 }).catch(() => {
|
|
106296
|
+
});
|
|
106297
|
+
}
|
|
106298
|
+
if (receipts.some((r) => r.cancelled)) return;
|
|
106299
|
+
this.takeStartedTurn(id2);
|
|
106300
|
+
for (const receipt of receipts) this.inputReceipts.bind(receipt, id2);
|
|
106301
|
+
}
|
|
105830
106302
|
/**
|
|
105831
106303
|
* Takes a running turn's id from a response, unless the notifications already named one or already ended it. The
|
|
105832
106304
|
* response and the notifications race: `turn/started` can come after the response (Stop pressed very quickly), and
|
|
@@ -105894,7 +106366,14 @@ var CodexSession = class {
|
|
|
105894
106366
|
}
|
|
105895
106367
|
return out;
|
|
105896
106368
|
}
|
|
105897
|
-
interrupt() {
|
|
106369
|
+
interrupt(reason = new Error("The parent note was stopped before it finished")) {
|
|
106370
|
+
const turns = this.inputReceipts.failAll(reason);
|
|
106371
|
+
this.pendingInputs = this.pendingInputs.filter((p) => !p.receipt);
|
|
106372
|
+
for (const turnId of turns) {
|
|
106373
|
+
if (!this.threadId || turnId === this.turnId || this.endedTurns.includes(turnId)) continue;
|
|
106374
|
+
void this.client.request("turn/interrupt", { threadId: this.threadId, turnId }).catch(() => {
|
|
106375
|
+
});
|
|
106376
|
+
}
|
|
105898
106377
|
this.opts.apps?.cancelAll();
|
|
105899
106378
|
if (!this.threadId && this.sendsIssued > this.sendsStopped) {
|
|
105900
106379
|
this.sendsStopped = this.sendsIssued;
|
|
@@ -105927,6 +106406,8 @@ var CodexSession = class {
|
|
|
105927
106406
|
}
|
|
105928
106407
|
/** The app-server is gone, and the child threads with it — each still listed ends as stopped. */
|
|
105929
106408
|
releaseChildren() {
|
|
106409
|
+
this.inputGroups.clear();
|
|
106410
|
+
this.inputReceipts.failAll(new Error("The session process ended before the parent note finished"));
|
|
105930
106411
|
for (const e of this.children.release("Ended with the session process")) this.emit(e);
|
|
105931
106412
|
}
|
|
105932
106413
|
/**
|
|
@@ -106068,7 +106549,7 @@ function threadIdOf(res) {
|
|
|
106068
106549
|
}
|
|
106069
106550
|
function codexHome() {
|
|
106070
106551
|
const custom2 = process.env.CODEX_HOME?.trim();
|
|
106071
|
-
return custom2 ? custom2 :
|
|
106552
|
+
return custom2 ? custom2 : join24(homedir10(), ".codex");
|
|
106072
106553
|
}
|
|
106073
106554
|
var CODEX_VERBOSITIES = ["low", "medium", "high"];
|
|
106074
106555
|
var CodexAdapter = class {
|
|
@@ -106102,6 +106583,8 @@ var CodexAdapter = class {
|
|
|
106102
106583
|
processModel: "per-session",
|
|
106103
106584
|
builtInSubagents: true,
|
|
106104
106585
|
peerMessaging: false,
|
|
106586
|
+
// 0.160 TurnStartParams: turn/start steers an already-active regular turn; compact/review input is queued.
|
|
106587
|
+
midTurnInput: true,
|
|
106105
106588
|
/*
|
|
106106
106589
|
* Not put to sleep (#568). Waking a large thread measured 7-13 s (`thread/resume` scales with the rollout, ~13.5 s for
|
|
106107
106590
|
* 550 MB), and its cache on resume is unmeasured. The memory is better won by one shared app-server per account
|
|
@@ -106122,7 +106605,7 @@ var CodexAdapter = class {
|
|
|
106122
106605
|
const launch2 = launchFor(path2 ?? "codex");
|
|
106123
106606
|
const { stdout } = await exec4(launch2.command, [...launch2.args, "--version"], { timeout: 5e3 });
|
|
106124
106607
|
const version2 = `${stdout.trim()} \xB7 ${path2 ?? "PATH"}`;
|
|
106125
|
-
const loggedIn =
|
|
106608
|
+
const loggedIn = existsSync17(join24(codexHome(), "auth.json"));
|
|
106126
106609
|
return {
|
|
106127
106610
|
tool: "codex",
|
|
106128
106611
|
installed: true,
|
|
@@ -106256,7 +106739,7 @@ function registeredTool(adapters2, preferred) {
|
|
|
106256
106739
|
|
|
106257
106740
|
// packages/agent-host/src/dev-services/store.ts
|
|
106258
106741
|
import Database from "better-sqlite3";
|
|
106259
|
-
import { existsSync as
|
|
106742
|
+
import { existsSync as existsSync18, readFileSync as readFileSync12 } from "node:fs";
|
|
106260
106743
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
106261
106744
|
function resolveSchemaPath() {
|
|
106262
106745
|
const candidates = [
|
|
@@ -106265,7 +106748,7 @@ function resolveSchemaPath() {
|
|
|
106265
106748
|
new URL("../../../protocol/src/schema/schema.sql", import.meta.url)
|
|
106266
106749
|
// source tree
|
|
106267
106750
|
].map((u) => fileURLToPath4(u));
|
|
106268
|
-
const found = candidates.find((p) =>
|
|
106751
|
+
const found = candidates.find((p) => existsSync18(p));
|
|
106269
106752
|
if (!found) throw new Error(`schema.sql not found: ${candidates.join(", ")}`);
|
|
106270
106753
|
return found;
|
|
106271
106754
|
}
|
|
@@ -106543,7 +107026,7 @@ var Store = class _Store {
|
|
|
106543
107026
|
const probe = Object.create(_Store.prototype);
|
|
106544
107027
|
const steps = probe.migrationSteps();
|
|
106545
107028
|
const known = steps[steps.length - 1].to;
|
|
106546
|
-
if (dbPath2 === ":memory:" || !
|
|
107029
|
+
if (dbPath2 === ":memory:" || !existsSync18(dbPath2)) return empty(known);
|
|
106547
107030
|
const db = new Database(dbPath2, { readonly: true, fileMustExist: true });
|
|
106548
107031
|
try {
|
|
106549
107032
|
probe.db = db;
|
|
@@ -108344,7 +108827,7 @@ var Store = class _Store {
|
|
|
108344
108827
|
this.db.prepare(`DELETE FROM sessions WHERE id = ? AND deleted_at IS NOT NULL`).run(sessionId);
|
|
108345
108828
|
return true;
|
|
108346
108829
|
});
|
|
108347
|
-
while (!step()) await new Promise((
|
|
108830
|
+
while (!step()) await new Promise((resolve12) => setImmediate(resolve12));
|
|
108348
108831
|
return true;
|
|
108349
108832
|
}
|
|
108350
108833
|
/** One session's messages in seq order, one transaction per chunk, letting the event loop go between chunks (#179) */
|
|
@@ -108359,7 +108842,7 @@ var Store = class _Store {
|
|
|
108359
108842
|
if (rows.length > 0) after = rows[rows.length - 1].seq;
|
|
108360
108843
|
return rows.length < chunk;
|
|
108361
108844
|
});
|
|
108362
|
-
while (!step()) await new Promise((
|
|
108845
|
+
while (!step()) await new Promise((resolve12) => setImmediate(resolve12));
|
|
108363
108846
|
}
|
|
108364
108847
|
isTrashed(sessionId) {
|
|
108365
108848
|
return !!this.db.prepare(`SELECT 1 FROM sessions WHERE id = ? AND deleted_at IS NOT NULL`).get(sessionId);
|
|
@@ -109026,7 +109509,8 @@ function parseTrashRecord(json2) {
|
|
|
109026
109509
|
projectName: str8(r.projectName),
|
|
109027
109510
|
projectPath: str8(r.projectPath),
|
|
109028
109511
|
removeExternal: r.removeExternal === true,
|
|
109029
|
-
removeWorktree: r.removeWorktree === true
|
|
109512
|
+
removeWorktree: r.removeWorktree === true,
|
|
109513
|
+
...r.recovery && typeof r.recovery === "object" && typeof r.recovery.snapshot === "string" && typeof r.recovery.tip === "string" && typeof r.recovery.changedFiles === "number" && typeof r.recovery.commits === "number" ? { recovery: r.recovery } : {}
|
|
109030
109514
|
};
|
|
109031
109515
|
}
|
|
109032
109516
|
var OUT_OF_TRASH = `session_id NOT IN (SELECT id FROM sessions WHERE deleted_at IS NOT NULL)`;
|
|
@@ -109051,7 +109535,7 @@ function indexedText(kind, payload) {
|
|
|
109051
109535
|
|
|
109052
109536
|
// packages/agent-host/src/dev-services/git.ts
|
|
109053
109537
|
import { execFile as execFile6 } from "node:child_process";
|
|
109054
|
-
import { isAbsolute as isAbsolute8, relative as relative7, resolve as
|
|
109538
|
+
import { isAbsolute as isAbsolute8, relative as relative7, resolve as resolve10, sep as sep9 } from "node:path";
|
|
109055
109539
|
import { promisify as promisify6 } from "node:util";
|
|
109056
109540
|
var exec5 = promisify6(execFile6);
|
|
109057
109541
|
var OK2 = { timeout: 1e4, maxBuffer: 32 * 1024 * 1024 };
|
|
@@ -109123,8 +109607,8 @@ function assertLexicalGitPath(cwd, path2) {
|
|
|
109123
109607
|
if (!path2 || path2.includes("\0") || path2.startsWith(":")) {
|
|
109124
109608
|
throw Object.assign(new Error("Invalid git path"), { code: "internal" });
|
|
109125
109609
|
}
|
|
109126
|
-
const root3 =
|
|
109127
|
-
const target = isAbsolute8(path2) ?
|
|
109610
|
+
const root3 = resolve10(cwd);
|
|
109611
|
+
const target = isAbsolute8(path2) ? resolve10(path2) : resolve10(root3, path2);
|
|
109128
109612
|
const rel = relative7(root3, target);
|
|
109129
109613
|
if (rel === "") throw Object.assign(new Error("Path is the project root, not a file in it"), { code: "internal" });
|
|
109130
109614
|
if (rel !== ".." && !rel.startsWith(`..${sep9}`) && !isAbsolute8(rel)) return rel;
|
|
@@ -109243,14 +109727,14 @@ async function gitIgnoredEntries(cwd, limit = 50, trust = {}) {
|
|
|
109243
109727
|
if (paths.length === 0) return [];
|
|
109244
109728
|
const sizes = /* @__PURE__ */ new Map();
|
|
109245
109729
|
try {
|
|
109246
|
-
const out = await new Promise((
|
|
109730
|
+
const out = await new Promise((resolve12, reject) => {
|
|
109247
109731
|
execFile6(
|
|
109248
109732
|
"du",
|
|
109249
109733
|
["-sk", ...paths.slice(0, limit)],
|
|
109250
109734
|
{ cwd, timeout: 5e3, maxBuffer: 1 << 20 },
|
|
109251
109735
|
(err, stdout) => (
|
|
109252
109736
|
// du exits non-zero if even one entry cannot be read — whatever it did print is used anyway
|
|
109253
|
-
stdout ?
|
|
109737
|
+
stdout ? resolve12(stdout) : reject(err)
|
|
109254
109738
|
)
|
|
109255
109739
|
);
|
|
109256
109740
|
});
|
|
@@ -109363,8 +109847,8 @@ async function gitValidBranchName(name) {
|
|
|
109363
109847
|
return false;
|
|
109364
109848
|
}
|
|
109365
109849
|
}
|
|
109366
|
-
async function gitWorktreeRemove(repoCwd, path2, force = false) {
|
|
109367
|
-
await git(repoCwd, ["worktree", "remove", ...force ? ["--force"] : [], path2]);
|
|
109850
|
+
async function gitWorktreeRemove(repoCwd, path2, force = false, withSubmodules = false) {
|
|
109851
|
+
await git(repoCwd, ["worktree", "remove", ...force ? ["--force", ...withSubmodules ? ["--force"] : []] : [], path2]);
|
|
109368
109852
|
}
|
|
109369
109853
|
async function gitBranchDelete(repoCwd, branch, trust = {}) {
|
|
109370
109854
|
await git(repoCwd, ["branch", "-D", branch], trust);
|
|
@@ -109380,13 +109864,13 @@ async function gitWorktreeDirty(path2, trust = {}) {
|
|
|
109380
109864
|
}
|
|
109381
109865
|
|
|
109382
109866
|
// packages/agent-host/src/dev-services/worktree-dependencies.ts
|
|
109383
|
-
import { lstat as
|
|
109384
|
-
import { join as
|
|
109867
|
+
import { lstat as lstat5, readFile as readFile4, readdir as readdir6 } from "node:fs/promises";
|
|
109868
|
+
import { join as join25 } from "node:path";
|
|
109385
109869
|
var LOCKFILES = ["pnpm-lock.yaml", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "bun.lock", "bun.lockb"];
|
|
109386
109870
|
async function prepareWorktreeDependencies(projectCwd, worktreeCwd) {
|
|
109387
109871
|
let locked = false;
|
|
109388
109872
|
for (const name of LOCKFILES) {
|
|
109389
|
-
const target = await
|
|
109873
|
+
const target = await lstat5(join25(worktreeCwd, name)).catch((err) => {
|
|
109390
109874
|
if (err.code === "ENOENT") return null;
|
|
109391
109875
|
throw err;
|
|
109392
109876
|
});
|
|
@@ -109395,7 +109879,7 @@ async function prepareWorktreeDependencies(projectCwd, worktreeCwd) {
|
|
|
109395
109879
|
resolveExisting(projectCwd, name),
|
|
109396
109880
|
resolveExisting(worktreeCwd, name)
|
|
109397
109881
|
]);
|
|
109398
|
-
const [before, after] = await Promise.all([
|
|
109882
|
+
const [before, after] = await Promise.all([readFile4(source), readFile4(destination)]);
|
|
109399
109883
|
if (!before.equals(after)) return;
|
|
109400
109884
|
locked = true;
|
|
109401
109885
|
}
|
|
@@ -109404,22 +109888,22 @@ async function prepareWorktreeDependencies(projectCwd, worktreeCwd) {
|
|
|
109404
109888
|
const folders = [""];
|
|
109405
109889
|
while (folders.length) {
|
|
109406
109890
|
const folder = folders.pop();
|
|
109407
|
-
const entries = await
|
|
109891
|
+
const entries = await readdir6(join25(worktreeCwd, folder), { withFileTypes: true });
|
|
109408
109892
|
for (const entry of entries) {
|
|
109409
109893
|
if (entry.isDirectory() && entry.name !== ".git" && entry.name !== "node_modules") {
|
|
109410
|
-
folders.push(
|
|
109894
|
+
folders.push(join25(folder, entry.name));
|
|
109411
109895
|
}
|
|
109412
109896
|
}
|
|
109413
109897
|
if (folder && !entries.some((e) => e.name === "package.json" && e.isFile())) continue;
|
|
109414
109898
|
if (entries.some((e) => e.name === "node_modules")) continue;
|
|
109415
|
-
const rel =
|
|
109899
|
+
const rel = join25(folder, "node_modules");
|
|
109416
109900
|
try {
|
|
109417
109901
|
const src = await resolveExisting(projectCwd, rel);
|
|
109418
|
-
if (!(await
|
|
109419
|
-
const manifest =
|
|
109902
|
+
if (!(await lstat5(src)).isDirectory()) continue;
|
|
109903
|
+
const manifest = join25(folder, "package.json");
|
|
109420
109904
|
const [source, target] = await Promise.all([
|
|
109421
|
-
resolveExisting(projectCwd, manifest).then((p) =>
|
|
109422
|
-
resolveExisting(worktreeCwd, manifest).then((p) =>
|
|
109905
|
+
resolveExisting(projectCwd, manifest).then((p) => readFile4(p)),
|
|
109906
|
+
resolveExisting(worktreeCwd, manifest).then((p) => readFile4(p))
|
|
109423
109907
|
]);
|
|
109424
109908
|
if (!source.equals(target)) continue;
|
|
109425
109909
|
const dst = await prepareCopyTarget(worktreeCwd, rel);
|
|
@@ -109441,8 +109925,8 @@ async function prepareWorktreeDependencies(projectCwd, worktreeCwd) {
|
|
|
109441
109925
|
}
|
|
109442
109926
|
|
|
109443
109927
|
// packages/agent-host/src/dev-services/file-search.ts
|
|
109444
|
-
import { readdir as
|
|
109445
|
-
import { join as
|
|
109928
|
+
import { readdir as readdir7 } from "node:fs/promises";
|
|
109929
|
+
import { join as join26 } from "node:path";
|
|
109446
109930
|
var MAX_FILES2 = 2e4;
|
|
109447
109931
|
var TTL_MS2 = 15e3;
|
|
109448
109932
|
var WALK_DEPTH = 6;
|
|
@@ -109470,7 +109954,7 @@ async function walk(root3) {
|
|
|
109470
109954
|
const { dir, rel, depth } = queue.shift();
|
|
109471
109955
|
let entries;
|
|
109472
109956
|
try {
|
|
109473
|
-
entries = await
|
|
109957
|
+
entries = await readdir7(dir, { withFileTypes: true });
|
|
109474
109958
|
} catch {
|
|
109475
109959
|
continue;
|
|
109476
109960
|
}
|
|
@@ -109478,7 +109962,7 @@ async function walk(root3) {
|
|
|
109478
109962
|
if (e.name.startsWith(".") && e.name !== ".env") continue;
|
|
109479
109963
|
if (e.isDirectory()) {
|
|
109480
109964
|
if (SKIP.has(e.name) || depth >= WALK_DEPTH) continue;
|
|
109481
|
-
queue.push({ dir:
|
|
109965
|
+
queue.push({ dir: join26(dir, e.name), rel: wireJoin(rel, e.name), depth: depth + 1 });
|
|
109482
109966
|
} else {
|
|
109483
109967
|
out.push(wireJoin(rel, e.name).normalize("NFC"));
|
|
109484
109968
|
if (out.length >= MAX_FILES2) break;
|
|
@@ -109543,14 +110027,14 @@ async function searchFiles(root3, query, limit = 20, trust = {}) {
|
|
|
109543
110027
|
}
|
|
109544
110028
|
|
|
109545
110029
|
// packages/agent-host/src/dev-services/attachments.ts
|
|
109546
|
-
import { mkdir as
|
|
109547
|
-
import { extname as extname3, join as
|
|
109548
|
-
var root = () =>
|
|
110030
|
+
import { mkdir as mkdir5, writeFile as writeFile3, rm as rm5 } from "node:fs/promises";
|
|
110031
|
+
import { extname as extname3, join as join27 } from "node:path";
|
|
110032
|
+
var root = () => join27(dataRoot(), "attachments");
|
|
109549
110033
|
function sessionDir(sessionId) {
|
|
109550
110034
|
if (!isSessionId(sessionId)) {
|
|
109551
110035
|
throw Object.assign(new Error(`Not a session id: ${sessionId}`), { code: "internal" });
|
|
109552
110036
|
}
|
|
109553
|
-
return
|
|
110037
|
+
return join27(root(), sessionId);
|
|
109554
110038
|
}
|
|
109555
110039
|
var EXT = {
|
|
109556
110040
|
"image/png": ".png",
|
|
@@ -109560,43 +110044,43 @@ var EXT = {
|
|
|
109560
110044
|
};
|
|
109561
110045
|
async function saveAttachment(sessionId, name, mime, dataBase64) {
|
|
109562
110046
|
const dir = sessionDir(sessionId);
|
|
109563
|
-
await
|
|
110047
|
+
await mkdir5(dir, { recursive: true });
|
|
109564
110048
|
const ext2 = extname3(name) || EXT[mime] || "";
|
|
109565
|
-
const file2 =
|
|
110049
|
+
const file2 = join27(dir, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext2}`);
|
|
109566
110050
|
const buf = Buffer.from(dataBase64, "base64");
|
|
109567
|
-
await
|
|
110051
|
+
await writeFile3(file2, buf);
|
|
109568
110052
|
return { kind: mime.startsWith("image/") ? "image" : "file", path: file2, name, mime, bytes: buf.length };
|
|
109569
110053
|
}
|
|
109570
110054
|
async function clearAttachments(sessionId) {
|
|
109571
|
-
await
|
|
110055
|
+
await rm5(sessionDir(sessionId), { recursive: true, force: true });
|
|
109572
110056
|
}
|
|
109573
110057
|
async function attachmentBytes(sessionId) {
|
|
109574
|
-
const { readdir:
|
|
110058
|
+
const { readdir: readdir10, lstat: lstat7 } = await import("node:fs/promises");
|
|
109575
110059
|
const dir = sessionDir(sessionId);
|
|
109576
|
-
const names = await
|
|
110060
|
+
const names = await readdir10(dir).catch(() => []);
|
|
109577
110061
|
let total = 0;
|
|
109578
110062
|
for (const name of names) {
|
|
109579
|
-
const s = await
|
|
110063
|
+
const s = await lstat7(join27(dir, name)).catch(() => null);
|
|
109580
110064
|
if (s?.isFile()) total += s.size;
|
|
109581
110065
|
}
|
|
109582
110066
|
return total;
|
|
109583
110067
|
}
|
|
109584
110068
|
var ATTACHMENTS_MAX_BYTES = 500 * 1048576;
|
|
109585
110069
|
async function sweepAttachments(maxBytes = ATTACHMENTS_MAX_BYTES) {
|
|
109586
|
-
const { readdir:
|
|
110070
|
+
const { readdir: readdir10, stat: stat7, rm: rmFile } = await import("node:fs/promises");
|
|
109587
110071
|
const files = [];
|
|
109588
110072
|
let dirs;
|
|
109589
110073
|
try {
|
|
109590
|
-
dirs = await
|
|
110074
|
+
dirs = await readdir10(root());
|
|
109591
110075
|
} catch {
|
|
109592
110076
|
return 0;
|
|
109593
110077
|
}
|
|
109594
110078
|
for (const d2 of dirs) {
|
|
109595
|
-
const dir =
|
|
109596
|
-
const names = await
|
|
110079
|
+
const dir = join27(root(), d2);
|
|
110080
|
+
const names = await readdir10(dir).catch(() => []);
|
|
109597
110081
|
for (const name of names) {
|
|
109598
|
-
const p =
|
|
109599
|
-
const s = await
|
|
110082
|
+
const p = join27(dir, name);
|
|
110083
|
+
const s = await stat7(p).catch(() => null);
|
|
109600
110084
|
if (s?.isFile()) files.push({ path: p, size: s.size, mtime: s.mtimeMs });
|
|
109601
110085
|
}
|
|
109602
110086
|
}
|
|
@@ -109616,7 +110100,7 @@ async function sweepAttachments(maxBytes = ATTACHMENTS_MAX_BYTES) {
|
|
|
109616
110100
|
|
|
109617
110101
|
// packages/agent-host/src/dev-services/message-image.ts
|
|
109618
110102
|
import { constants as constants7 } from "node:fs";
|
|
109619
|
-
import { open as open4, realpath as
|
|
110103
|
+
import { open as open4, realpath as realpath7 } from "node:fs/promises";
|
|
109620
110104
|
import { homedir as homedir11 } from "node:os";
|
|
109621
110105
|
import { posix as posix3, sep as sep10, win32 as win326 } from "node:path";
|
|
109622
110106
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
@@ -109673,7 +110157,7 @@ var mb2 = (n) => `${(n / 1e6).toFixed(1)} MB`;
|
|
|
109673
110157
|
async function readMessageImage(abs, max = IMAGE_PREVIEW_MAX_BYTES) {
|
|
109674
110158
|
let file2;
|
|
109675
110159
|
try {
|
|
109676
|
-
file2 = await
|
|
110160
|
+
file2 = await realpath7(abs);
|
|
109677
110161
|
} catch (e) {
|
|
109678
110162
|
const code2 = e.code;
|
|
109679
110163
|
if (code2 === "EACCES" || code2 === "EPERM") return refuse5("unreadable", `Centralu may not read ${abs}`);
|
|
@@ -109712,36 +110196,36 @@ async function readMessageImage(abs, max = IMAGE_PREVIEW_MAX_BYTES) {
|
|
|
109712
110196
|
}
|
|
109713
110197
|
|
|
109714
110198
|
// packages/agent-host/src/dev-services/handoff-notes.ts
|
|
109715
|
-
import { lstat as
|
|
109716
|
-
import { join as
|
|
109717
|
-
var root2 = () =>
|
|
110199
|
+
import { lstat as lstat6, mkdir as mkdir6, readdir as readdir8, rm as rm6, writeFile as writeFile4 } from "node:fs/promises";
|
|
110200
|
+
import { join as join28 } from "node:path";
|
|
110201
|
+
var root2 = () => join28(dataRoot(), "handoff");
|
|
109718
110202
|
function handoffNoteDir(projectId) {
|
|
109719
110203
|
if (!isProjectId(projectId)) {
|
|
109720
110204
|
throw Object.assign(new Error(`Not a project id: ${projectId}`), { code: "internal" });
|
|
109721
110205
|
}
|
|
109722
|
-
return
|
|
110206
|
+
return join28(root2(), projectId);
|
|
109723
110207
|
}
|
|
109724
110208
|
function handoffNotePath(projectId, sessionId) {
|
|
109725
110209
|
if (!isSessionId(sessionId)) {
|
|
109726
110210
|
throw Object.assign(new Error(`Not a session id: ${sessionId}`), { code: "internal" });
|
|
109727
110211
|
}
|
|
109728
|
-
return
|
|
110212
|
+
return join28(handoffNoteDir(projectId), `${sessionId}.md`);
|
|
109729
110213
|
}
|
|
109730
110214
|
async function handoffNoteBytes(projectId, sessionId) {
|
|
109731
|
-
const s = await
|
|
110215
|
+
const s = await lstat6(handoffNotePath(projectId, sessionId)).catch(() => null);
|
|
109732
110216
|
return s?.isFile() ? s.size : 0;
|
|
109733
110217
|
}
|
|
109734
110218
|
async function writeHandoffNote(projectId, sessionId, text2) {
|
|
109735
110219
|
const path2 = handoffNotePath(projectId, sessionId);
|
|
109736
|
-
await
|
|
109737
|
-
await
|
|
109738
|
-
await
|
|
110220
|
+
await mkdir6(handoffNoteDir(projectId), { recursive: true });
|
|
110221
|
+
await rm6(path2, { force: true });
|
|
110222
|
+
await writeFile4(path2, text2, "utf8");
|
|
109739
110223
|
return path2;
|
|
109740
110224
|
}
|
|
109741
110225
|
async function sweepHandoffNotes(owned, projectId) {
|
|
109742
110226
|
let dirs;
|
|
109743
110227
|
try {
|
|
109744
|
-
dirs = projectId ? [projectId] : (await
|
|
110228
|
+
dirs = projectId ? [projectId] : (await readdir8(root2(), { withFileTypes: true })).filter((d2) => d2.isDirectory()).map((d2) => d2.name);
|
|
109745
110229
|
} catch {
|
|
109746
110230
|
return;
|
|
109747
110231
|
}
|
|
@@ -109752,12 +110236,12 @@ async function sweepHandoffNotes(owned, projectId) {
|
|
|
109752
110236
|
} catch {
|
|
109753
110237
|
continue;
|
|
109754
110238
|
}
|
|
109755
|
-
const entries = await
|
|
110239
|
+
const entries = await readdir8(dir, { withFileTypes: true }).catch(() => []);
|
|
109756
110240
|
for (const e of entries) {
|
|
109757
110241
|
if (!e.isFile() || !e.name.endsWith(".md")) continue;
|
|
109758
110242
|
const owner = e.name.slice(0, -".md".length);
|
|
109759
110243
|
if (owned(owner)) continue;
|
|
109760
|
-
await
|
|
110244
|
+
await rm6(join28(dir, e.name), { force: true }).catch(() => {
|
|
109761
110245
|
});
|
|
109762
110246
|
}
|
|
109763
110247
|
}
|
|
@@ -109831,7 +110315,7 @@ var HUB_SENDER = "_hub-orchestrator";
|
|
|
109831
110315
|
var builderKey = (ref) => `${ref.projectId ?? "_user"}/${ref.appId}`;
|
|
109832
110316
|
var APP_BUILDERS_KEY = "apps.builders";
|
|
109833
110317
|
var SessionManager = class {
|
|
109834
|
-
constructor(store2, adapters2, emit, endpoint, worktreeRoot =
|
|
110318
|
+
constructor(store2, adapters2, emit, endpoint, worktreeRoot = join29(homedir12(), DATA_DIR, "worktrees"), keeperOpts = {}) {
|
|
109835
110319
|
this.store = store2;
|
|
109836
110320
|
this.adapters = adapters2;
|
|
109837
110321
|
this.emit = emit;
|
|
@@ -109844,10 +110328,17 @@ var SessionManager = class {
|
|
|
109844
110328
|
rawEmit(withoutToolRecord(full));
|
|
109845
110329
|
};
|
|
109846
110330
|
const LIVE_ONLY = ["working", "waiting_approval"];
|
|
109847
|
-
|
|
110331
|
+
const storedSessions = store2.listSessions();
|
|
110332
|
+
const storedProjects = new Map(storedSessions.map((s) => [s.id, s.projectId]));
|
|
110333
|
+
for (const s of storedSessions) {
|
|
109848
110334
|
const stale = LIVE_ONLY.includes(s.state) && !keeperOpts.keptSessions?.has(s.id);
|
|
109849
110335
|
const fixed = stale ? { ...s, state: "idle", waitingSince: null } : s;
|
|
110336
|
+
const ownerProject = fixed.askedBy ? storedProjects.get(fixed.askedBy) ?? store2.trashedSession(fixed.askedBy)?.record.projectId : void 0;
|
|
110337
|
+
const first = fixed.askedBy && ownerProject === void 0 ? store2.loadMessagesFrom(fixed.id, 0, 1)[0]?.payload : void 0;
|
|
110338
|
+
const oldAsk = fixed.askedBy && (ownerProject !== void 0 ? ownerProject !== fixed.projectId : first?.from?.sessionId === fixed.askedBy && first.text?.includes("through Centralu's ask_project"));
|
|
110339
|
+
if (oldAsk) delete fixed.askedBy;
|
|
109850
110340
|
this.meta.set(s.id, fixed);
|
|
110341
|
+
if (oldAsk) store2.upsertSession(fixed);
|
|
109851
110342
|
if (stale) {
|
|
109852
110343
|
console.error(`[agent-host] stale state reset: ${s.id.slice(0, 8)} ${s.state} -> idle`);
|
|
109853
110344
|
store2.upsertSession(fixed);
|
|
@@ -109950,6 +110441,8 @@ var SessionManager = class {
|
|
|
109950
110441
|
* app-server), even though the screen said "starting next turn".
|
|
109951
110442
|
*/
|
|
109952
110443
|
restartAfterTurn = /* @__PURE__ */ new Set();
|
|
110444
|
+
/** Input arriving after a note completes waits for its deferred process replacement, including disposal. */
|
|
110445
|
+
deferredRestarts = /* @__PURE__ */ new Map();
|
|
109953
110446
|
/** A new host served a different Centralu tool prefix; replace the old agent at its safe boundary. */
|
|
109954
110447
|
definitionsRestartAfterTurn = /* @__PURE__ */ new Set();
|
|
109955
110448
|
/** A kept process was started with adapter settings from an older host build (#589). */
|
|
@@ -109986,6 +110479,8 @@ var SessionManager = class {
|
|
|
109986
110479
|
* comment on `runAppAgent`).
|
|
109987
110480
|
*/
|
|
109988
110481
|
agentRuns = /* @__PURE__ */ new Map();
|
|
110482
|
+
/** Temporary readers of a person's turn; they never own work or pin the calling session awake. */
|
|
110483
|
+
turnObservers = /* @__PURE__ */ new Map();
|
|
109989
110484
|
/**
|
|
109990
110485
|
* A card the host raises in a session's approval slot — requestId -> question: an app's capability question (M4
|
|
109991
110486
|
* D-4), or one project reaching another (#371). It uses the same slot as the adapter's card (the session's
|
|
@@ -109994,8 +110489,8 @@ var SessionManager = class {
|
|
|
109994
110489
|
*/
|
|
109995
110490
|
hostAsks = /* @__PURE__ */ new Map();
|
|
109996
110491
|
/**
|
|
109997
|
-
* ask_project's delegations still owed to their caller (#371 part B) — `<caller session>><target project>` -> the
|
|
109998
|
-
* delegation. One per
|
|
110492
|
+
* ask_project's delegations still owed to their caller (#371 part B) — `<caller session>><target project>><target session>` -> the
|
|
110493
|
+
* delegation. One per chosen session at a time: a second task while the first runs is refused, and a call with no task waits
|
|
109999
110494
|
* on this one. Removed once the caller has read the outcome; an outcome nobody comes back for goes with the caller.
|
|
110000
110495
|
*/
|
|
110001
110496
|
delegations = /* @__PURE__ */ new Map();
|
|
@@ -110057,11 +110552,11 @@ var SessionManager = class {
|
|
|
110057
110552
|
);
|
|
110058
110553
|
}
|
|
110059
110554
|
async addProject(path2) {
|
|
110060
|
-
if (!
|
|
110555
|
+
if (!existsSync19(path2) || !statSync12(path2).isDirectory()) {
|
|
110061
110556
|
throw Object.assign(new Error(`Directory not found: ${path2}`), { code: "internal" });
|
|
110062
110557
|
}
|
|
110063
110558
|
const existing = this.store.findProjectByPath(path2);
|
|
110064
|
-
const id2 = existing?.id ??
|
|
110559
|
+
const id2 = existing?.id ?? randomUUID10();
|
|
110065
110560
|
this.store.addProject({ id: id2, path: path2, name: basename5(path2) });
|
|
110066
110561
|
return this.projectInfo(id2, path2);
|
|
110067
110562
|
}
|
|
@@ -110073,8 +110568,8 @@ var SessionManager = class {
|
|
|
110073
110568
|
projectsHome = () => {
|
|
110074
110569
|
throw Object.assign(new Error("This host has no default location for a new project \u2014 give a parent folder"), { code: "internal" });
|
|
110075
110570
|
};
|
|
110076
|
-
useProjectsHome(
|
|
110077
|
-
this.projectsHome =
|
|
110571
|
+
useProjectsHome(resolve12) {
|
|
110572
|
+
this.projectsHome = resolve12;
|
|
110078
110573
|
}
|
|
110079
110574
|
/**
|
|
110080
110575
|
* The folder a new project would get, and whether a clone source is one this host clones (#515) — checked, nothing
|
|
@@ -110575,6 +111070,9 @@ var SessionManager = class {
|
|
|
110575
111070
|
* null — the only place that can create a session with no project is `orchestrator()` below.
|
|
110576
111071
|
*/
|
|
110577
111072
|
async createSession(params) {
|
|
111073
|
+
if (params.askedBy && this.meta.get(params.askedBy)?.projectId !== params.projectId) {
|
|
111074
|
+
throw new Error("A session cannot create a session in another project");
|
|
111075
|
+
}
|
|
110578
111076
|
if (!params.tool) {
|
|
110579
111077
|
throw Object.assign(new Error("No agent adapter is available. Install and configure an agent CLI before creating a session."), { code: "tool_not_installed" });
|
|
110580
111078
|
}
|
|
@@ -110589,7 +111087,7 @@ var SessionManager = class {
|
|
|
110589
111087
|
);
|
|
110590
111088
|
}
|
|
110591
111089
|
}
|
|
110592
|
-
const id2 =
|
|
111090
|
+
const id2 = randomUUID10();
|
|
110593
111091
|
let worktree = null;
|
|
110594
111092
|
if (params.worktree && params.projectId) {
|
|
110595
111093
|
const summary = await gitSummary(params.cwd, this.gitTrust(params.projectId));
|
|
@@ -110602,7 +111100,7 @@ var SessionManager = class {
|
|
|
110602
111100
|
);
|
|
110603
111101
|
}
|
|
110604
111102
|
const path2 = this.worktreePathFor(params.projectId, id2);
|
|
110605
|
-
if (!this.projectTrusted(params.projectId)) {
|
|
111103
|
+
if (!this.projectTrusted(params.projectId) && !(params.askedBy && this.autoBoundaryOpen(params.askedBy, "worktree in an untrusted project"))) {
|
|
110606
111104
|
throw Object.assign(
|
|
110607
111105
|
new Error("Worktree sessions need a trusted project: checking out runs the repository's hooks and filters. Trust the project first."),
|
|
110608
111106
|
{ code: "internal" }
|
|
@@ -110993,6 +111491,7 @@ var SessionManager = class {
|
|
|
110993
111491
|
);
|
|
110994
111492
|
}
|
|
110995
111493
|
async doResumeSession(sessionId, adopt) {
|
|
111494
|
+
if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
110996
111495
|
const m = this.meta.get(sessionId);
|
|
110997
111496
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
110998
111497
|
if (this.handles.has(sessionId)) return { session: m, resumed: true };
|
|
@@ -111181,6 +111680,7 @@ var SessionManager = class {
|
|
|
111181
111680
|
* exactly as it was, and only what happens from here on piles up in the copy.
|
|
111182
111681
|
*/
|
|
111183
111682
|
async forkConversation(sessionId) {
|
|
111683
|
+
if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
111184
111684
|
const m = this.meta.get(sessionId);
|
|
111185
111685
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
111186
111686
|
const source = m.externalId ?? m.importedFrom;
|
|
@@ -111231,7 +111731,8 @@ var SessionManager = class {
|
|
|
111231
111731
|
* delete knows that layout (the Claude SDK's `deleteSession`). Left in place it is also a second way back — the
|
|
111232
111732
|
* tool, and **+ → Past conversations**, still have it.
|
|
111233
111733
|
*/
|
|
111234
|
-
async trashSession(sessionId, removeWorktree = false, removeExternal = false) {
|
|
111734
|
+
async trashSession(sessionId, removeWorktree = false, removeExternal = false, recovery) {
|
|
111735
|
+
if (this.worktreeCleanup.has(sessionId) && !recovery) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
111235
111736
|
const m = this.meta.get(sessionId);
|
|
111236
111737
|
if (!m) {
|
|
111237
111738
|
this.emit({ type: "session_deleted", sessionId });
|
|
@@ -111247,6 +111748,7 @@ var SessionManager = class {
|
|
|
111247
111748
|
agentRun.deleted = true;
|
|
111248
111749
|
agentRun.fail(new Error("the person deleted the agent session before it answered"));
|
|
111249
111750
|
}
|
|
111751
|
+
for (const observer of this.turnObservers.get(sessionId) ?? []) observer.fail(new Error("the person deleted the session before it answered"));
|
|
111250
111752
|
for (const [key, d2] of this.delegations) {
|
|
111251
111753
|
if (d2.callerId !== sessionId) continue;
|
|
111252
111754
|
this.delegations.delete(key);
|
|
@@ -111280,7 +111782,8 @@ var SessionManager = class {
|
|
|
111280
111782
|
projectName: project2?.name ?? null,
|
|
111281
111783
|
projectPath: project2?.path ?? null,
|
|
111282
111784
|
removeExternal: removeExternal && !!externalId,
|
|
111283
|
-
removeWorktree: removeWorktree && !!m.worktree
|
|
111785
|
+
removeWorktree: removeWorktree && !!m.worktree,
|
|
111786
|
+
...recovery ? { recovery } : {}
|
|
111284
111787
|
});
|
|
111285
111788
|
this.appsHub?.sessionGone(sessionId);
|
|
111286
111789
|
this.emit({ type: "session_deleted", sessionId });
|
|
@@ -111288,7 +111791,7 @@ var SessionManager = class {
|
|
|
111288
111791
|
/** Only one trash operation per session at a time — a purge racing a restore would restore half of a deleted session */
|
|
111289
111792
|
trashBusy = /* @__PURE__ */ new Set();
|
|
111290
111793
|
async trashOp(sessionId, run) {
|
|
111291
|
-
if (this.trashBusy.has(sessionId)) {
|
|
111794
|
+
if (this.trashBusy.has(sessionId) || this.worktreeCleanup.has(sessionId)) {
|
|
111292
111795
|
throw Object.assign(new Error("That session is already being restored or deleted"), { code: "internal" });
|
|
111293
111796
|
}
|
|
111294
111797
|
this.trashBusy.add(sessionId);
|
|
@@ -111305,7 +111808,7 @@ var SessionManager = class {
|
|
|
111305
111808
|
this.store.listTrash().map(async (r) => {
|
|
111306
111809
|
const { record: record2 } = r;
|
|
111307
111810
|
const live = record2.projectId ? projects.get(record2.projectId) : void 0;
|
|
111308
|
-
const files = await attachmentBytes(r.id).catch(() => 0) + (record2.projectId ? await handoffNoteBytes(record2.projectId, r.id).catch(() => 0) : 0);
|
|
111811
|
+
const files = await attachmentBytes(r.id).catch(() => 0) + await worktreeRecoveryBytes(r.id).catch(() => 0) + (record2.projectId ? await handoffNoteBytes(record2.projectId, r.id).catch(() => 0) : 0);
|
|
111309
111812
|
return {
|
|
111310
111813
|
id: r.id,
|
|
111311
111814
|
name: r.name,
|
|
@@ -111321,7 +111824,7 @@ var SessionManager = class {
|
|
|
111321
111824
|
bytes: r.bytes + files,
|
|
111322
111825
|
conversationFile: !r.hasConversationFile ? "none" : record2.removeExternal ? "remove" : "keep",
|
|
111323
111826
|
// Only a worktree still on disk is listed — the merged-worktree cleanup removes the folder before the trash is emptied
|
|
111324
|
-
worktree: r.worktree &&
|
|
111827
|
+
worktree: r.worktree && existsSync19(r.worktree.path) ? { ...r.worktree, remove: record2.removeWorktree } : null
|
|
111325
111828
|
};
|
|
111326
111829
|
})
|
|
111327
111830
|
);
|
|
@@ -111359,7 +111862,7 @@ var SessionManager = class {
|
|
|
111359
111862
|
const home = projects.find((p) => p.id === record2.projectId) ?? (record2.projectPath ? projects.find((p) => p.path === record2.projectPath) : void 0);
|
|
111360
111863
|
if (home) {
|
|
111361
111864
|
projectId = home.id;
|
|
111362
|
-
} else if (record2.projectPath &&
|
|
111865
|
+
} else if (record2.projectPath && existsSync19(record2.projectPath) && statSync12(record2.projectPath).isDirectory()) {
|
|
111363
111866
|
this.store.addProject({
|
|
111364
111867
|
id: record2.projectId,
|
|
111365
111868
|
path: record2.projectPath,
|
|
@@ -111378,14 +111881,21 @@ var SessionManager = class {
|
|
|
111378
111881
|
} else if (t.session.kind === "orchestrator" && this.store.orchestratorId()) {
|
|
111379
111882
|
throw Object.assign(new Error("Another orchestrator is in place \u2014 the app runs one at a time"), { code: "internal" });
|
|
111380
111883
|
}
|
|
111884
|
+
let recovered;
|
|
111885
|
+
if (record2.recovery && t.session.worktree && projectId) {
|
|
111886
|
+
const repo = this.store.listProjects().find((p) => p.id === projectId).path;
|
|
111887
|
+
recovered = await restoreWorktreeRecovery(sessionId, repo, t.session.worktree.path, t.session.worktree.branch, record2.recovery, this.gitTrust(projectId));
|
|
111888
|
+
}
|
|
111381
111889
|
const back = await this.store.restoreSession(sessionId, projectId);
|
|
111382
111890
|
if (!back) throw Object.assign(new Error(`Not in the trash: ${sessionId}`), { code: "session_not_found" });
|
|
111383
111891
|
const LIVE_ONLY = ["working", "waiting_approval"];
|
|
111384
111892
|
const fixed = LIVE_ONLY.includes(back.state) ? { ...back, state: "idle", waitingSince: null } : back;
|
|
111385
|
-
if (
|
|
111893
|
+
if (recovered && fixed.worktree) fixed.worktree = { ...fixed.worktree, branch: recovered.branch };
|
|
111894
|
+
if (fixed !== back || recovered) this.store.upsertSession(fixed);
|
|
111386
111895
|
this.meta.set(sessionId, fixed);
|
|
111387
111896
|
const session2 = { ...this.meta.get(sessionId), live: false };
|
|
111388
111897
|
this.emit({ type: "session_created", sessionId, session: session2 });
|
|
111898
|
+
if (recovered) this.onEvent({ type: "notice", sessionId, level: "info", from: "Centralu", label: "worktree recovery", text: recovered.notice });
|
|
111389
111899
|
return { session: session2, project: registered };
|
|
111390
111900
|
});
|
|
111391
111901
|
}
|
|
@@ -111424,6 +111934,7 @@ var SessionManager = class {
|
|
|
111424
111934
|
await gitWorktreeRemove(record2.projectPath, s.worktree.path, true).catch(() => {
|
|
111425
111935
|
});
|
|
111426
111936
|
}
|
|
111937
|
+
await clearWorktreeRecovery(sessionId);
|
|
111427
111938
|
await this.store.purgeSession(sessionId);
|
|
111428
111939
|
await clearAttachments(sessionId).catch(() => {
|
|
111429
111940
|
});
|
|
@@ -111496,6 +112007,7 @@ var SessionManager = class {
|
|
|
111496
112007
|
return live && m && live.model !== m.model ? "model_change" : void 0;
|
|
111497
112008
|
}
|
|
111498
112009
|
async updateSettings(sessionId, s) {
|
|
112010
|
+
if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
111499
112011
|
const m = this.meta.get(sessionId);
|
|
111500
112012
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
111501
112013
|
if (s.model !== void 0 && s.model !== m.model) this.cacheLedger.note(sessionId, "model_change");
|
|
@@ -111610,20 +112122,31 @@ var SessionManager = class {
|
|
|
111610
112122
|
}
|
|
111611
112123
|
}
|
|
111612
112124
|
}
|
|
112125
|
+
const run = e.sessionId ? this.agentRuns.get(e.sessionId) : void 0;
|
|
112126
|
+
run?.onEvent(e);
|
|
112127
|
+
if (e.sessionId) for (const observer of this.turnObservers.get(e.sessionId) ?? []) observer.onEvent(e);
|
|
111613
112128
|
this.emit(seq != null ? { ...e, seq } : e);
|
|
111614
|
-
if (endedTurn && e.sessionId) void this.reportBackIfAwaited(e.sessionId, e);
|
|
111615
|
-
if (e.sessionId) this.agentRuns.get(e.sessionId)?.onEvent(e);
|
|
112129
|
+
if (endedTurn && e.sessionId && !run?.tracksInputs) void this.reportBackIfAwaited(e.sessionId, e, run?.completedAnswer);
|
|
111616
112130
|
if (e.type === "approval_resolved" && e.sessionId) this.raiseHostAsks(e.sessionId);
|
|
111617
112131
|
if (endedTurn && e.sessionId) this.forgetCommits(e.sessionId);
|
|
111618
|
-
if (endedTurn && e.sessionId
|
|
111619
|
-
|
|
111620
|
-
|
|
111621
|
-
|
|
111622
|
-
|
|
111623
|
-
|
|
111624
|
-
|
|
111625
|
-
|
|
111626
|
-
|
|
112132
|
+
if (endedTurn && e.sessionId) this.applyDeferredRestart(e.sessionId);
|
|
112133
|
+
}
|
|
112134
|
+
/** A note receipt follows turn_complete: re-check the same deferred change when the last hold is released. */
|
|
112135
|
+
applyDeferredRestart(sessionId) {
|
|
112136
|
+
const session2 = this.meta.get(sessionId);
|
|
112137
|
+
if (!session2 || inTurn(session2.state) || this.deferredRestarts.has(sessionId) || this.agentRuns.get(sessionId)?.waitingForInputs || !this.restartAfterTurn.delete(sessionId)) return;
|
|
112138
|
+
const apps = this.appsRestartAfterTurn.delete(sessionId);
|
|
112139
|
+
const definitions = this.definitionsRestartAfterTurn.delete(sessionId);
|
|
112140
|
+
const launchSettings = this.launchSettingsRestartAfterTurn.delete(sessionId);
|
|
112141
|
+
if (!(apps || definitions || launchSettings || this.settingsDrifted(sessionId)) || !this.handles.has(sessionId)) return;
|
|
112142
|
+
const cause2 = launchSettings ? "launch_settings" : apps ? "thread_restart" : definitions ? "instructions" : this.restartCause(sessionId);
|
|
112143
|
+
const restarting = Promise.resolve().then(() => this.restartSession(sessionId, cause2)).then(() => {
|
|
112144
|
+
}).finally(() => {
|
|
112145
|
+
if (this.deferredRestarts.get(sessionId) === restarting) this.deferredRestarts.delete(sessionId);
|
|
112146
|
+
});
|
|
112147
|
+
this.deferredRestarts.set(sessionId, restarting);
|
|
112148
|
+
void restarting.catch(() => {
|
|
112149
|
+
});
|
|
111627
112150
|
}
|
|
111628
112151
|
/**
|
|
111629
112152
|
* Notifies the orchestrator when the work it assigned finishes — **the other half of "one window".**
|
|
@@ -111637,14 +112160,14 @@ var SessionManager = class {
|
|
|
111637
112160
|
* - Once notified, the marker is cleared. Otherwise that session would wake the orchestrator on
|
|
111638
112161
|
* every turn it runs on its own afterward — which would become a loop of waking each other
|
|
111639
112162
|
*/
|
|
111640
|
-
async reportBackIfAwaited(sessionId, ending) {
|
|
112163
|
+
async reportBackIfAwaited(sessionId, ending, answer) {
|
|
111641
112164
|
const orchestratorId = this.awaitingReport.get(sessionId);
|
|
111642
112165
|
if (!orchestratorId) return;
|
|
111643
112166
|
this.awaitingReport.delete(sessionId);
|
|
111644
112167
|
const target = this.meta.get(sessionId);
|
|
111645
112168
|
if (!target || !this.meta.has(orchestratorId)) return;
|
|
111646
112169
|
const project2 = target.projectId ? this.store.listProjects().find((p) => p.id === target.projectId)?.name ?? "(project no longer exists)" : "(none)";
|
|
111647
|
-
await this.deliverReport(orchestratorId, sessionId, target.name, project2, previewOf(this.store, sessionId, 600), ending);
|
|
112170
|
+
await this.deliverReport(orchestratorId, sessionId, target.name, project2, answer?.slice(0, 600) ?? previewOf(this.store, sessionId, 600), ending);
|
|
111648
112171
|
}
|
|
111649
112172
|
/** Reports the end of a turn the orchestrator asked to hear about, as one message in its session (#166) */
|
|
111650
112173
|
async deliverReport(orchestratorId, sessionId, name, project2, preview, ending) {
|
|
@@ -111980,7 +112503,10 @@ If you need more, you can read that session's recent conversation with read_sess
|
|
|
111980
112503
|
* exposed on send(), which anything else can call — a boundary that can be flipped on by accident
|
|
111981
112504
|
* is not a boundary.
|
|
111982
112505
|
*/
|
|
111983
|
-
async deliver(sessionId, text2, attachments, from, relayed, fromApp, fromAppVia = "inline") {
|
|
112506
|
+
async deliver(sessionId, text2, attachments, from, relayed, fromApp, fromAppVia = "inline", sendInput) {
|
|
112507
|
+
const restarting = this.deferredRestarts.get(sessionId);
|
|
112508
|
+
if (restarting) await restarting;
|
|
112509
|
+
if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
111984
112510
|
const m = this.meta.get(sessionId);
|
|
111985
112511
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
111986
112512
|
if (!this.handles.has(sessionId)) {
|
|
@@ -111991,6 +112517,7 @@ If you need more, you can read that session's recent conversation with read_sess
|
|
|
111991
112517
|
});
|
|
111992
112518
|
}
|
|
111993
112519
|
}
|
|
112520
|
+
if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
111994
112521
|
const h = this.requireHandle(sessionId);
|
|
111995
112522
|
this.closeStream(sessionId);
|
|
111996
112523
|
const seq = this.store.nextSeq(sessionId);
|
|
@@ -112027,7 +112554,8 @@ If you need more, you can read that session's recent conversation with read_sess
|
|
|
112027
112554
|
const adapterText = fromApp ? appMessageFrame(fromApp, text2, fromAppVia) : from && (relayed || this.directs(sessionId) && !this.directs(from.sessionId) && from.sessionId !== HUB_SENDER) ? untrustedSourceSessionNotification(from.sessionId) : attachments?.length ? `${text2}
|
|
112028
112555
|
|
|
112029
112556
|
${attachments.map((a) => `@${a.path}`).join("\n")}` : text2;
|
|
112030
|
-
h
|
|
112557
|
+
if (sendInput) sendInput(h, adapterText);
|
|
112558
|
+
else h.send(adapterText);
|
|
112031
112559
|
}
|
|
112032
112560
|
/**
|
|
112033
112561
|
* Switches a session's agent (claude <-> codex).
|
|
@@ -112044,6 +112572,7 @@ ${attachments.map((a) => `@${a.path}`).join("\n")}` : text2;
|
|
|
112044
112572
|
* untouched, and only the tool underneath it is swapped.
|
|
112045
112573
|
*/
|
|
112046
112574
|
async switchTool(sessionId, tool) {
|
|
112575
|
+
if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
112047
112576
|
const m = this.meta.get(sessionId);
|
|
112048
112577
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
112049
112578
|
if (m.tool === tool) return { ...m, live: this.handles.has(sessionId) };
|
|
@@ -112325,7 +112854,7 @@ ${attachments.map((a) => `@${a.path}`).join("\n")}` : text2;
|
|
|
112325
112854
|
worktreePathFor(projectId, sessionId) {
|
|
112326
112855
|
if (!isProjectId(projectId)) throw Object.assign(new Error(`Not a project id: ${projectId}`), { code: "internal" });
|
|
112327
112856
|
if (!isSessionId(sessionId)) throw Object.assign(new Error(`Not a session id: ${sessionId}`), { code: "internal" });
|
|
112328
|
-
return
|
|
112857
|
+
return join29(this.worktreeRoot, projectId, sessionId);
|
|
112329
112858
|
}
|
|
112330
112859
|
/**
|
|
112331
112860
|
* Sets up a new worktree's workspace (#69): copy configured files, reuse JS dependencies, run setup.
|
|
@@ -112508,7 +113037,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
112508
113037
|
const base2 = this.cwdFor(m);
|
|
112509
113038
|
const paths = /* @__PURE__ */ new Set();
|
|
112510
113039
|
for (const p of e.paths) {
|
|
112511
|
-
const rel = relative8(root3,
|
|
113040
|
+
const rel = relative8(root3, resolve11(base2, p));
|
|
112512
113041
|
if (!rel || rel === ".." || rel.startsWith(`..${sep11}`) || isAbsolute9(rel)) continue;
|
|
112513
113042
|
paths.add(rel.replaceAll(sep11, "/"));
|
|
112514
113043
|
}
|
|
@@ -112708,8 +113237,10 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
112708
113237
|
* reports background work.
|
|
112709
113238
|
*/
|
|
112710
113239
|
sessionIdle(sessionId) {
|
|
113240
|
+
if (this.worktreeCleanup.has(sessionId)) return { idle: false, reason: "turn" };
|
|
112711
113241
|
const m = this.meta.get(sessionId);
|
|
112712
113242
|
if (!m) throw Object.assign(new Error(`Session not found: ${sessionId}`), { code: "session_not_found" });
|
|
113243
|
+
if (this.agentRuns.get(sessionId)?.waitingForInputs) return { idle: false, reason: "turn" };
|
|
112713
113244
|
return sessionIdle(m, {
|
|
112714
113245
|
running: this.handles.has(sessionId),
|
|
112715
113246
|
reportsBackground: !!this.adapters.get(m.tool)?.capabilities.backgroundTasks
|
|
@@ -112717,7 +113248,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
112717
113248
|
}
|
|
112718
113249
|
/**
|
|
112719
113250
|
* The sessions whose running work this one waits on (#568, the sleep rule in sleep.ts): a child it started with
|
|
112720
|
-
* `start_agent`, or a session
|
|
113251
|
+
* `start_agent`, or a session handling its ask_project task, while the turn it gave them runs, and a session that owes
|
|
112721
113252
|
* it a report (`reportBack`, `awaitingReport`). Their answer is about to come back to it.
|
|
112722
113253
|
*/
|
|
112723
113254
|
waitsOn(sessionId) {
|
|
@@ -112737,7 +113268,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
112737
113268
|
*/
|
|
112738
113269
|
async sleepSession(sessionId) {
|
|
112739
113270
|
const h = this.handles.get(sessionId);
|
|
112740
|
-
if (!h || this.resuming.has(sessionId) || this.sleeping.has(sessionId) || this.leaving.has(sessionId)) return false;
|
|
113271
|
+
if (!h || this.resuming.has(sessionId) || this.deferredRestarts.has(sessionId) || this.sleeping.has(sessionId) || this.leaving.has(sessionId)) return false;
|
|
112741
113272
|
if (!this.sessionIdle(sessionId).idle || this.waitsOn(sessionId).length > 0) return false;
|
|
112742
113273
|
this.handles.delete(sessionId);
|
|
112743
113274
|
this.running.delete(sessionId);
|
|
@@ -112757,12 +113288,14 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
112757
113288
|
return true;
|
|
112758
113289
|
}
|
|
112759
113290
|
interrupt(sessionId) {
|
|
112760
|
-
this.requireHandle(sessionId)
|
|
112761
|
-
for (const d2 of this.delegations.values()) {
|
|
112762
|
-
if (d2.callerId === sessionId && (!d2.agent || (d2.waiters ?? 0) > 0)) d2.stop("the person stopped the session that asked");
|
|
112763
|
-
}
|
|
113291
|
+
const handle = this.requireHandle(sessionId);
|
|
112764
113292
|
const run = this.agentRuns.get(sessionId);
|
|
112765
113293
|
if (run) run.stoppedByPerson = true;
|
|
113294
|
+
for (const observer of this.turnObservers.get(sessionId) ?? []) observer.stoppedByPerson = true;
|
|
113295
|
+
handle.interrupt();
|
|
113296
|
+
for (const d2 of this.delegations.values()) {
|
|
113297
|
+
if (d2.callerId === sessionId && (d2.waiters ?? 0) > 0) d2.stop("the person stopped the session that asked");
|
|
113298
|
+
}
|
|
112766
113299
|
}
|
|
112767
113300
|
/**
|
|
112768
113301
|
* Restarts only the agent (an extension of FR-10).
|
|
@@ -112770,6 +113303,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
112770
113303
|
* process.
|
|
112771
113304
|
*/
|
|
112772
113305
|
async restartSession(sessionId, cause2) {
|
|
113306
|
+
if (this.worktreeCleanup.has(sessionId)) throw Object.assign(new Error("The session worktree is being removed"), { code: "internal" });
|
|
112773
113307
|
if (cause2) {
|
|
112774
113308
|
this.restartReasons.set(sessionId, cause2);
|
|
112775
113309
|
if (cause2 === "launch_settings") console.error(`[sessions] restarting ${sessionId}: launch settings changed or previously unrecorded`);
|
|
@@ -113377,10 +113911,11 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113377
113911
|
kind: "project_access",
|
|
113378
113912
|
access: access2,
|
|
113379
113913
|
from: { id: from.id, name: from.name },
|
|
113380
|
-
// Said on the card (#382):
|
|
113914
|
+
// Said on the card (#382): the selected session there follows its trust, so the person should know before allowing
|
|
113381
113915
|
to: { id: to.id, name: to.name, trusted: to.trusted },
|
|
113382
113916
|
text: what.text,
|
|
113383
|
-
...what.app ? { app: what.app } : {}
|
|
113917
|
+
...what.app ? { app: what.app } : {},
|
|
113918
|
+
...what.session ? { session: what.session } : {}
|
|
113384
113919
|
},
|
|
113385
113920
|
signal
|
|
113386
113921
|
);
|
|
@@ -113395,20 +113930,26 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113395
113930
|
}
|
|
113396
113931
|
return { ok: false, error: `The question to the person was withdrawn before they answered, so ${to.name} was not reached.` };
|
|
113397
113932
|
}
|
|
113398
|
-
/**
|
|
113399
|
-
|
|
113400
|
-
|
|
113401
|
-
|
|
113933
|
+
/** The caller's own preset, read at the boundary, never its project's or parent's preset. */
|
|
113934
|
+
autoBoundaryOpen(sessionId, boundary) {
|
|
113935
|
+
if (this.meta.get(sessionId)?.permissionPreset !== "auto") return false;
|
|
113936
|
+
if (boundary) console.error(`[permissions] auto session ${sessionId}: ${boundary} allowed`);
|
|
113937
|
+
return true;
|
|
113938
|
+
}
|
|
113939
|
+
/** Auto allows and logs; other presets raise a host card, withdrawn if the signal fires. */
|
|
113402
113940
|
askHost(sessionId, detail, signal) {
|
|
113403
|
-
return
|
|
113404
|
-
|
|
113405
|
-
|
|
113406
|
-
|
|
113941
|
+
if (signal?.aborted) return Promise.resolve(null);
|
|
113942
|
+
const note = detail.kind === "project_access" && !detail.to.trusted ? " (target not trusted)" : "";
|
|
113943
|
+
if (this.autoBoundaryOpen(sessionId, `${detail.kind}${note}`)) return Promise.resolve("allow");
|
|
113944
|
+
return new Promise((resolve12) => {
|
|
113945
|
+
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 });
|
|
113407
113948
|
signal?.addEventListener("abort", () => {
|
|
113408
113949
|
if (!this.hostAsks.delete(requestId)) return;
|
|
113409
113950
|
const m = this.meta.get(sessionId);
|
|
113410
113951
|
if (m?.pendingApproval?.requestId === requestId) this.onEvent({ type: "approval_resolved", sessionId, requestId, decision: "deny" });
|
|
113411
|
-
|
|
113952
|
+
resolve12(null);
|
|
113412
113953
|
}, { once: true });
|
|
113413
113954
|
this.raiseHostAsks(sessionId);
|
|
113414
113955
|
});
|
|
@@ -113440,25 +113981,24 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113440
113981
|
* and a session that was itself asked by another project cannot ask a third: depth one, so two projects
|
|
113441
113982
|
* that consented to each other cannot bounce a task between them without a person.
|
|
113442
113983
|
* 2. The person's consent for the pair (`ensureProjectAccess`, kind 'delegate').
|
|
113443
|
-
* 3.
|
|
113444
|
-
*
|
|
113445
|
-
* gets, so its own approvals stand as cards in that session, where the person sees them. Marked `askedBy`.
|
|
113984
|
+
* 3. An existing ordinary session in the target, named by name or id; no creation or ownership change.
|
|
113985
|
+
* Without a name exactly one is selected, otherwise its sessions are listed for the caller to choose.
|
|
113446
113986
|
* 4. The task goes in a frame (`askFrame`) marked as sent by the caller, and the call waits for the turn, up to
|
|
113447
113987
|
* `ASK_WAIT_MS`. Past it the answer is "still working" and a call with no task waits again.
|
|
113448
113988
|
* 5. The final answer comes back cut to size, and the paths it names inside the target project become readable
|
|
113449
113989
|
* to the caller (`readableGrants`).
|
|
113450
113990
|
*
|
|
113451
113991
|
* Stop on the caller, the call's own signal, or the person deleting either session ends the wait; the first two
|
|
113452
|
-
* stop the
|
|
113992
|
+
* stop an assigned idle target only while waiting; a task that joined the person's turn only ends the wait.
|
|
113453
113993
|
*/
|
|
113454
113994
|
async askProject(callerId, opts, signal) {
|
|
113455
113995
|
const caller = this.meta.get(callerId);
|
|
113456
113996
|
if (!caller?.projectId) return { ok: false, error: "This session belongs to no project, so it cannot ask another one" };
|
|
113457
|
-
if (caller.askedBy) {
|
|
113997
|
+
if ((caller.askedBy || [...this.delegations.values()].some((d8) => d8.sessionId === callerId && !d8.agent && !d8.settled)) && !this.autoBoundaryOpen(callerId, "delegation depth")) {
|
|
113458
113998
|
return { ok: false, error: "This session was itself started or asked by another session, so it cannot ask a project \u2014 say in your answer what is needed instead." };
|
|
113459
113999
|
}
|
|
113460
114000
|
const projects = this.store.listProjects();
|
|
113461
|
-
if (projects.find((p) => p.id === caller.projectId)?.trusted !== true) {
|
|
114001
|
+
if (projects.find((p) => p.id === caller.projectId)?.trusted !== true && !this.autoBoundaryOpen(callerId, "delegate from an untrusted project")) {
|
|
113462
114002
|
return { ok: false, error: "This session's project is not trusted, so it cannot ask another project \u2014 the person can trust it from the project menu." };
|
|
113463
114003
|
}
|
|
113464
114004
|
const wanted = opts.project.trim();
|
|
@@ -113471,112 +114011,203 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113471
114011
|
};
|
|
113472
114012
|
}
|
|
113473
114013
|
if (target.id === caller.projectId) return { ok: false, error: `${target.name} is this session's own project \u2014 do the work here.` };
|
|
113474
|
-
const
|
|
114014
|
+
const candidates = [...this.meta.values()].filter(
|
|
114015
|
+
(s) => s.projectId === target.id && s.kind === "worker" && !s.appId && !this.builderRefOf(s)
|
|
114016
|
+
);
|
|
114017
|
+
const list = () => candidates.map((s) => JSON.stringify({
|
|
114018
|
+
id: s.id,
|
|
114019
|
+
name: s.name,
|
|
114020
|
+
state: s.state,
|
|
114021
|
+
lastActive: lastActiveOf(this.store, s.id) ?? null
|
|
114022
|
+
})).join("\n");
|
|
114023
|
+
const wantedSession = opts.session?.trim();
|
|
114024
|
+
const outstanding = [...this.delegations.entries()].filter(
|
|
114025
|
+
([, d8]) => d8.callerId === callerId && d8.target.id === target.id && !d8.agent
|
|
114026
|
+
);
|
|
114027
|
+
if (!opts.task && !wantedSession && outstanding.length === 1) {
|
|
114028
|
+
const [key2, pending2] = outstanding[0];
|
|
114029
|
+
return this.awaitDelegation(key2, pending2, signal);
|
|
114030
|
+
}
|
|
114031
|
+
const named = wantedSession ? candidates.filter((s) => s.name.toLowerCase() === wantedSession.toLowerCase()) : candidates;
|
|
114032
|
+
const chosen = candidates.find((s) => s.id === wantedSession) ?? (named.length === 1 ? named[0] : void 0);
|
|
114033
|
+
if (!chosen) {
|
|
114034
|
+
return { ok: false, error: candidates.length ? `${wantedSession ? `No unique ordinary session named "${wantedSession}" in ${target.name}. ` : ""}Name a session with the session argument. Sessions (name, state, last active):
|
|
114035
|
+
${list()}` : `${target.name} has no ordinary session. The person has to open one there; this call cannot create one.` };
|
|
114036
|
+
}
|
|
114037
|
+
const key = `${callerId}>${target.id}>${chosen.id}`;
|
|
113475
114038
|
const pending = this.delegations.get(key);
|
|
113476
114039
|
if (pending && !opts.task) return this.awaitDelegation(key, pending, signal);
|
|
113477
114040
|
if (pending && !pending.settled) {
|
|
113478
|
-
return {
|
|
113479
|
-
|
|
113480
|
-
|
|
113481
|
-
|
|
113482
|
-
|
|
113483
|
-
|
|
113484
|
-
|
|
113485
|
-
|
|
114041
|
+
return { ok: false, error: `${target.name} is still working on your last task in the session "${chosen.name}". Call ask_project with project "${target.name}", session "${chosen.id}" and no task to wait for it.` };
|
|
114042
|
+
}
|
|
114043
|
+
if (!opts.task) return { ok: false, error: `Give a task: what ${target.name} / ${chosen.name} should do.` };
|
|
114044
|
+
const available = () => {
|
|
114045
|
+
const current = this.meta.get(chosen.id);
|
|
114046
|
+
if (!current || current.projectId !== target.id || current.kind !== "worker" || current.appId || this.builderRefOf(current)) return "The chosen session is no longer available";
|
|
114047
|
+
if (this.agentRuns.has(chosen.id) && !this.agentRuns.get(chosen.id).finished) return `"${current.name}" is busy (${current.state}) with another delegated task. Wait for it.`;
|
|
114048
|
+
if (inTurn(current.state) && (!this.adapters.get(current.tool)?.capabilities.midTurnInput || !this.handles.get(current.id)?.sendTracked)) return `"${current.name}" is busy (${current.state}); its CLI cannot take a mid-turn note. Try again later.`;
|
|
114049
|
+
};
|
|
114050
|
+
const busy = available();
|
|
114051
|
+
if (busy) return { ok: false, error: busy };
|
|
114052
|
+
const consent = await this.ensureProjectAccess(callerId, target.id, "delegate", {
|
|
114053
|
+
text: taskLine(opts.task),
|
|
114054
|
+
session: { id: chosen.id, name: chosen.name }
|
|
114055
|
+
}, signal);
|
|
113486
114056
|
if (!consent.ok) return consent;
|
|
113487
|
-
const
|
|
113488
|
-
|
|
113489
|
-
if (
|
|
113490
|
-
|
|
113491
|
-
|
|
113492
|
-
|
|
113493
|
-
|
|
113494
|
-
|
|
113495
|
-
const found = await adapter.detect();
|
|
113496
|
-
if (!found.installed || !found.loggedIn) {
|
|
113497
|
-
return { ok: false, error: `${target.name}'s agent (${adapter.descriptor.label}) cannot take the task: ${found.detail}` };
|
|
113498
|
-
}
|
|
113499
|
-
try {
|
|
113500
|
-
const remembered = this.store.projectToolDefaults(target.id)[tool];
|
|
113501
|
-
const info = await this.createSession({
|
|
113502
|
-
projectId: target.id,
|
|
113503
|
-
cwd: target.path,
|
|
113504
|
-
tool,
|
|
113505
|
-
...remembered?.model ? { model: remembered.model } : {},
|
|
113506
|
-
...remembered?.effort ? { effort: remembered.effort } : {},
|
|
113507
|
-
permissionPreset: "normal",
|
|
113508
|
-
askedBy: callerId
|
|
113509
|
-
});
|
|
113510
|
-
delegatedId = info.id;
|
|
113511
|
-
} catch (e) {
|
|
113512
|
-
return { ok: false, error: `Could not open a session in ${target.name}: ${e.message}` };
|
|
113513
|
-
}
|
|
113514
|
-
const callerProject2 = projects.find((p) => p.id === caller.projectId)?.name ?? "another project";
|
|
113515
|
-
this.rename(delegatedId, `Asked by ${callerProject2} \xB7 ${(/* @__PURE__ */ new Date()).toTimeString().slice(0, 5)}`);
|
|
113516
|
-
}
|
|
113517
|
-
const d2 = this.startDelegation(key, callerId, delegatedId, target);
|
|
114057
|
+
const changed = available();
|
|
114058
|
+
if (changed) return { ok: false, error: changed };
|
|
114059
|
+
if (signal?.aborted) return { ok: false, error: "The call was cancelled before delivery" };
|
|
114060
|
+
if (pending) this.delegations.delete(key);
|
|
114061
|
+
const delegatedId = chosen.id;
|
|
114062
|
+
const midTurn = inTurn(this.meta.get(delegatedId).state);
|
|
114063
|
+
let releaseInput;
|
|
114064
|
+
const d2 = this.startDelegation(key, callerId, delegatedId, target, false, midTurn);
|
|
113518
114065
|
const callerProject = projects.find((p) => p.id === caller.projectId)?.name ?? "another project";
|
|
113519
114066
|
try {
|
|
113520
|
-
|
|
114067
|
+
const from = { sessionId: callerId, name: labelOf(this.store, caller) };
|
|
114068
|
+
if (midTurn) {
|
|
114069
|
+
const releaseHold = d2.wait.holdInput();
|
|
114070
|
+
const release = () => {
|
|
114071
|
+
releaseHold();
|
|
114072
|
+
this.applyDeferredRestart(delegatedId);
|
|
114073
|
+
};
|
|
114074
|
+
releaseInput = release;
|
|
114075
|
+
await this.deliver(delegatedId, askFrame(callerProject, opts.task), void 0, from, false, void 0, "inline", (h, input) => {
|
|
114076
|
+
const fail4 = (e) => {
|
|
114077
|
+
d2.wait.fail(e);
|
|
114078
|
+
release();
|
|
114079
|
+
};
|
|
114080
|
+
void h.sendTracked(input, { complete: release, fail: fail4 }).then(release, fail4);
|
|
114081
|
+
});
|
|
114082
|
+
} else {
|
|
114083
|
+
await this.deliver(delegatedId, askFrame(callerProject, opts.task), void 0, from, false);
|
|
114084
|
+
}
|
|
113521
114085
|
} catch (e) {
|
|
113522
|
-
|
|
114086
|
+
d2.wait.fail(e);
|
|
114087
|
+
releaseInput?.();
|
|
114088
|
+
if (this.delegations.get(key) === d2) this.delegations.delete(key);
|
|
113523
114089
|
if (this.agentRuns.get(delegatedId) === d2.wait) this.agentRuns.delete(delegatedId);
|
|
113524
114090
|
return { ok: false, error: `Could not hand the task to ${target.name}: ${e.message}` };
|
|
113525
114091
|
}
|
|
113526
114092
|
return this.awaitDelegation(key, d2, signal);
|
|
113527
114093
|
}
|
|
113528
114094
|
/** Registers the wait on the delegated session's turn (the same watcher an app's agent uses, `AgentRunWait`) */
|
|
113529
|
-
startDelegation(key, callerId, sessionId, target, agent = false) {
|
|
114095
|
+
startDelegation(key, callerId, sessionId, target, agent = false, joinedTurn = false) {
|
|
113530
114096
|
let notice;
|
|
113531
|
-
|
|
114097
|
+
let answerAfter = this.meta.get(sessionId)?.lastSeq ?? 0;
|
|
114098
|
+
const wait = new AgentRunWait((message) => notice = message, () => this.meta.get(sessionId)?.name ?? "the session", void 0, () => {
|
|
114099
|
+
const answer = finalAnswer(this.store.loadMessages(sessionId, 50).filter((m) => m.seq > answerAfter));
|
|
114100
|
+
answerAfter = this.meta.get(sessionId)?.lastSeq ?? answerAfter;
|
|
114101
|
+
return answer;
|
|
114102
|
+
});
|
|
114103
|
+
let cancelWait;
|
|
114104
|
+
const waitCancelled = new Promise((resolve12) => {
|
|
114105
|
+
cancelWait = resolve12;
|
|
114106
|
+
});
|
|
113532
114107
|
const d2 = {
|
|
114108
|
+
joinedTurn,
|
|
114109
|
+
waitCancelled,
|
|
113533
114110
|
callerId,
|
|
113534
114111
|
sessionId,
|
|
113535
114112
|
target,
|
|
113536
114113
|
wait,
|
|
113537
|
-
settled
|
|
114114
|
+
get settled() {
|
|
114115
|
+
return wait.finished;
|
|
114116
|
+
},
|
|
113538
114117
|
notice: () => notice,
|
|
113539
114118
|
outcome: wait.done.then(
|
|
113540
|
-
() => ({ ok: true }),
|
|
114119
|
+
(r) => ({ ok: true, answer: r.answer }),
|
|
113541
114120
|
(e) => ({ ok: false, error: e.message })
|
|
113542
114121
|
),
|
|
113543
|
-
stop: (why) => {
|
|
114122
|
+
stop: (why, targetStop = false) => {
|
|
113544
114123
|
if (d2.settled) return;
|
|
114124
|
+
if (joinedTurn && !targetStop) {
|
|
114125
|
+
cancelWait({ ok: false, error: `${why}; the caller stopped waiting and the target session was left running` });
|
|
114126
|
+
return;
|
|
114127
|
+
}
|
|
114128
|
+
wait.fail(new Error(`${why}, so the delegated turn was stopped`));
|
|
113545
114129
|
try {
|
|
113546
114130
|
this.handles.get(sessionId)?.interrupt();
|
|
113547
114131
|
} catch {
|
|
113548
114132
|
}
|
|
113549
|
-
wait.fail(new Error(`${why}, so the delegated turn was stopped`));
|
|
113550
114133
|
},
|
|
113551
114134
|
...agent ? { agent: true } : {}
|
|
113552
114135
|
};
|
|
113553
|
-
void d2.outcome.then(() => {
|
|
113554
|
-
d2.settled = true;
|
|
114136
|
+
void d2.outcome.then((r) => {
|
|
113555
114137
|
if (this.agentRuns.get(sessionId) === wait) this.agentRuns.delete(sessionId);
|
|
114138
|
+
if (agent || wait.tracksInputs) void this.reportBackIfAwaited(sessionId, r.ok ? { type: "turn_complete", sessionId } : { type: "error", sessionId, error: { code: "internal", message: r.error, retryable: false } }, r.ok ? r.answer : void 0);
|
|
113556
114139
|
});
|
|
113557
114140
|
this.agentRuns.set(sessionId, wait);
|
|
113558
114141
|
this.delegations.set(key, d2);
|
|
113559
114142
|
return d2;
|
|
113560
114143
|
}
|
|
114144
|
+
/** Read an existing turn without claiming it in delegations, agentRuns or the caller's sleep graph. */
|
|
114145
|
+
observeTurn(child, subscribe) {
|
|
114146
|
+
const active = this.agentRuns.get(child.id);
|
|
114147
|
+
const shared = active && !active.finished ? active : void 0;
|
|
114148
|
+
const wait = shared ?? new AgentRunWait(
|
|
114149
|
+
() => {
|
|
114150
|
+
},
|
|
114151
|
+
() => child.name,
|
|
114152
|
+
void 0,
|
|
114153
|
+
() => finalAnswer(this.store.loadMessages(child.id, 50))
|
|
114154
|
+
);
|
|
114155
|
+
const project2 = this.store.listProjects().find((p) => p.id === child.projectId);
|
|
114156
|
+
const target = { id: child.projectId ?? "", name: project2?.name ?? "", path: this.store.sessionCwd(child.id) ?? project2?.path ?? "" };
|
|
114157
|
+
if (!shared && subscribe) {
|
|
114158
|
+
const readers = this.turnObservers.get(child.id) ?? /* @__PURE__ */ new Set();
|
|
114159
|
+
readers.add(wait);
|
|
114160
|
+
this.turnObservers.set(child.id, readers);
|
|
114161
|
+
}
|
|
114162
|
+
return {
|
|
114163
|
+
run: {
|
|
114164
|
+
// This is the target's own turn, never work owned by its observer.
|
|
114165
|
+
callerId: child.id,
|
|
114166
|
+
sessionId: child.id,
|
|
114167
|
+
target,
|
|
114168
|
+
wait,
|
|
114169
|
+
get settled() {
|
|
114170
|
+
return wait.finished;
|
|
114171
|
+
},
|
|
114172
|
+
notice: () => void 0,
|
|
114173
|
+
outcome: wait.done.then(
|
|
114174
|
+
(r) => ({ ok: true, answer: r.answer }),
|
|
114175
|
+
(e) => ({ ok: false, error: e.message })
|
|
114176
|
+
),
|
|
114177
|
+
stop: (why) => {
|
|
114178
|
+
if (wait.finished) return;
|
|
114179
|
+
wait.fail(new Error(`${why}, so the turn was stopped`));
|
|
114180
|
+
this.handles.get(child.id)?.interrupt();
|
|
114181
|
+
}
|
|
114182
|
+
},
|
|
114183
|
+
release: () => {
|
|
114184
|
+
if (shared) return;
|
|
114185
|
+
const readers = this.turnObservers.get(child.id);
|
|
114186
|
+
readers?.delete(wait);
|
|
114187
|
+
if (readers?.size === 0) this.turnObservers.delete(child.id);
|
|
114188
|
+
}
|
|
114189
|
+
};
|
|
114190
|
+
}
|
|
113561
114191
|
/** Waits for a delegation up to the bound, and turns its outcome into what the caller reads */
|
|
113562
114192
|
async awaitDelegation(key, d2, signal) {
|
|
113563
114193
|
const sessionName = () => this.meta.get(d2.sessionId)?.name ?? d2.sessionId;
|
|
113564
114194
|
let timer;
|
|
113565
114195
|
let onAbort;
|
|
113566
|
-
const bound = new Promise((
|
|
113567
|
-
const aborted2 = new Promise((
|
|
114196
|
+
const bound = new Promise((resolve12) => timer = setTimeout(() => resolve12("bound"), ASK_WAIT_MS));
|
|
114197
|
+
const aborted2 = new Promise((resolve12) => {
|
|
113568
114198
|
if (!signal) return;
|
|
113569
|
-
onAbort = () =>
|
|
114199
|
+
onAbort = () => resolve12("aborted");
|
|
113570
114200
|
if (signal.aborted) onAbort();
|
|
113571
114201
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
113572
114202
|
});
|
|
114203
|
+
d2.waiters = (d2.waiters ?? 0) + 1;
|
|
113573
114204
|
try {
|
|
113574
|
-
const r = await Promise.race([d2.outcome, bound, aborted2]);
|
|
114205
|
+
const r = await Promise.race([d2.outcome, ...d2.waitCancelled ? [d2.waitCancelled] : [], bound, aborted2]);
|
|
113575
114206
|
if (r === "aborted") {
|
|
113576
114207
|
d2.stop("the call that asked was cancelled");
|
|
113577
|
-
await d2.outcome;
|
|
113578
|
-
this.delegations.delete(key);
|
|
113579
|
-
return { ok: false, error: `The call was cancelled, so ${d2.target.name}'s session "${sessionName()}" was stopped.` };
|
|
114208
|
+
if (!d2.joinedTurn) await d2.outcome;
|
|
114209
|
+
if (this.delegations.get(key) === d2) this.delegations.delete(key);
|
|
114210
|
+
return { ok: false, error: d2.joinedTurn ? `The call was cancelled; ${d2.target.name}'s session "${sessionName()}" was left running.` : `The call was cancelled, so ${d2.target.name}'s session "${sessionName()}" was stopped.` };
|
|
113580
114211
|
}
|
|
113581
114212
|
if (r === "bound") {
|
|
113582
114213
|
const notice = d2.notice();
|
|
@@ -113586,10 +114217,10 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113586
114217
|
if (!r.ok) {
|
|
113587
114218
|
return {
|
|
113588
114219
|
ok: false,
|
|
113589
|
-
error: `${d2.target.name}'s session "${sessionName()}" [${d2.sessionId}] did not finish: ${r.error}. Tell the person, or ask again with a clearer task.`
|
|
114220
|
+
error: d2.joinedTurn ? `The wait for ${d2.target.name}'s session "${sessionName()}" [${d2.sessionId}] ended: ${r.error}.` : `${d2.target.name}'s session "${sessionName()}" [${d2.sessionId}] did not finish: ${r.error}. Tell the person, or ask again with a clearer task.`
|
|
113590
114221
|
};
|
|
113591
114222
|
}
|
|
113592
|
-
const answer = finalAnswer(this.store.loadMessages(d2.sessionId, 50));
|
|
114223
|
+
const answer = r.answer ?? finalAnswer(this.store.loadMessages(d2.sessionId, 50));
|
|
113593
114224
|
const { granted, outside } = readableGrants(pathsIn(answer), d2.target.path);
|
|
113594
114225
|
if (granted.length) {
|
|
113595
114226
|
const set3 = this.readGrants.get(d2.callerId) ?? /* @__PURE__ */ new Set();
|
|
@@ -113607,6 +114238,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113607
114238
|
outside
|
|
113608
114239
|
};
|
|
113609
114240
|
} finally {
|
|
114241
|
+
d2.waiters = (d2.waiters ?? 1) - 1;
|
|
113610
114242
|
clearTimeout(timer);
|
|
113611
114243
|
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
113612
114244
|
}
|
|
@@ -113628,19 +114260,19 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113628
114260
|
async startAgent(callerId, opts, signal) {
|
|
113629
114261
|
const caller = this.meta.get(callerId);
|
|
113630
114262
|
if (!caller?.projectId) return { ok: false, error: "This session belongs to no project, so it cannot start agents." };
|
|
113631
|
-
if (caller.askedBy) {
|
|
114263
|
+
if ((caller.askedBy || [...this.delegations.values()].some((d8) => d8.sessionId === callerId && !d8.agent && !d8.settled)) && !this.autoBoundaryOpen(callerId, "delegation depth")) {
|
|
113632
114264
|
return { ok: false, 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." };
|
|
113633
114265
|
}
|
|
113634
114266
|
const project2 = this.store.listProjects().find((p) => p.id === caller.projectId);
|
|
113635
114267
|
if (!project2) return { ok: false, error: "This session's project is no longer registered." };
|
|
113636
|
-
if (project2.trusted !== true) {
|
|
114268
|
+
if (project2.trusted !== true && !this.autoBoundaryOpen(callerId, "start in an untrusted project")) {
|
|
113637
114269
|
return { 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." };
|
|
113638
114270
|
}
|
|
113639
114271
|
const tool = opts.tool ?? this.firstTool(project2.defaultTool);
|
|
113640
114272
|
if (!tool) return { ok: false, error: "No agent adapter is available. Install and configure an agent CLI before starting an agent." };
|
|
113641
114273
|
const adapter = this.adapters.get(tool);
|
|
113642
114274
|
if (!adapter) return { ok: false, error: `No agent named "${tool}" here. Installed tools: ${[...this.adapters.keys()].join(", ") || "none"}.` };
|
|
113643
|
-
const full = this.agentCeilingError(project2.id);
|
|
114275
|
+
const full = this.agentCeilingError(callerId, project2.id);
|
|
113644
114276
|
if (full) return { ok: false, error: full };
|
|
113645
114277
|
const remembered = this.store.projectToolDefaults(project2.id)[tool];
|
|
113646
114278
|
const model = opts.model ?? remembered?.model ?? void 0;
|
|
@@ -113665,7 +114297,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113665
114297
|
signal
|
|
113666
114298
|
);
|
|
113667
114299
|
if (!consent.ok) return consent;
|
|
113668
|
-
const fullNow = this.agentCeilingError(project2.id);
|
|
114300
|
+
const fullNow = this.agentCeilingError(callerId, project2.id);
|
|
113669
114301
|
if (fullNow) return { ok: false, error: fullNow };
|
|
113670
114302
|
const cwd = worktree ? project2.path : this.store.sessionCwd(callerId) ?? caller.worktree?.path ?? project2.path;
|
|
113671
114303
|
let child;
|
|
@@ -113703,73 +114335,115 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113703
114335
|
return result.ok && notice ? { ...result, notice: [result.notice, notice].filter(Boolean).join("; ") } : result;
|
|
113704
114336
|
}
|
|
113705
114337
|
/**
|
|
113706
|
-
* agent_session (#561):
|
|
113707
|
-
* caller, in the caller's project.
|
|
114338
|
+
* agent_session (#561): safe/normal work only with their own children; auto opens ownership within the project.
|
|
113708
114339
|
*
|
|
113709
114340
|
* status where it stands, without waiting (the answer, once the turn the caller gave it has ended)
|
|
113710
114341
|
* wait waits on that turn up to the bound, as start_agent does
|
|
113711
|
-
* send gives it more
|
|
113712
|
-
*
|
|
114342
|
+
* send gives it more, including during the caller's turn on adapters that accept mid-turn input; refused
|
|
114343
|
+
* while it works on the person's own message or has a higher preset than the caller
|
|
113713
114344
|
* stop stops the turn the caller gave it; a turn the person started there is theirs
|
|
113714
|
-
* delete
|
|
114345
|
+
* delete sets aside local work in the trash, then removes an idle child's worktree and branch (#608)
|
|
113715
114346
|
*/
|
|
113716
114347
|
async agentSession(callerId, opts, signal) {
|
|
114348
|
+
if (opts.sessionId === callerId && opts.action !== "status") {
|
|
114349
|
+
return { ok: false, error: "agent_session cannot wait on, stop, delete or send to its own calling session." };
|
|
114350
|
+
}
|
|
113717
114351
|
const caller = this.meta.get(callerId);
|
|
113718
114352
|
const child = this.meta.get(opts.sessionId);
|
|
113719
|
-
if (!caller?.projectId || !child || child.askedBy !== callerId || child.projectId !== caller.projectId) {
|
|
114353
|
+
if (!caller?.projectId || !child || child.askedBy !== callerId && !this.autoBoundaryOpen(callerId, "session ownership") || child.projectId !== caller.projectId) {
|
|
113720
114354
|
return { ok: false, error: `${opts.sessionId} is not a session this session started. agent_session works only on sessions start_agent started here.` };
|
|
113721
114355
|
}
|
|
114356
|
+
if (opts.sessionId === callerId) return this.agentAtRest(child);
|
|
113722
114357
|
const key = `${callerId}>${child.id}`;
|
|
113723
|
-
|
|
113724
|
-
|
|
113725
|
-
|
|
113726
|
-
|
|
113727
|
-
|
|
113728
|
-
|
|
113729
|
-
|
|
113730
|
-
|
|
113731
|
-
|
|
113732
|
-
|
|
113733
|
-
|
|
113734
|
-
|
|
113735
|
-
|
|
113736
|
-
|
|
113737
|
-
|
|
113738
|
-
|
|
113739
|
-
|
|
113740
|
-
|
|
113741
|
-
|
|
113742
|
-
|
|
113743
|
-
|
|
113744
|
-
|
|
113745
|
-
|
|
113746
|
-
|
|
113747
|
-
|
|
113748
|
-
|
|
113749
|
-
|
|
113750
|
-
|
|
113751
|
-
return { ok: false, error: `"${child.name}" is
|
|
113752
|
-
|
|
113753
|
-
|
|
113754
|
-
|
|
113755
|
-
|
|
113756
|
-
error: `"${child.name}" now has more permissions (${child.permissionPreset}) than this session (${caller.permissionPreset}), so this session cannot give it work. Tell the person.`
|
|
113757
|
-
};
|
|
114358
|
+
let d2 = this.delegations.get(key);
|
|
114359
|
+
let releaseObservation;
|
|
114360
|
+
if (!d2 && this.autoBoundaryOpen(callerId)) {
|
|
114361
|
+
const existing = [...this.delegations.values()].find((run) => run.sessionId === child.id && !run.settled);
|
|
114362
|
+
if (existing) d2 = existing;
|
|
114363
|
+
else if (opts.action !== "delete" && (child.state === "working" || child.state === "waiting_approval")) {
|
|
114364
|
+
const observation = this.observeTurn(child, opts.action !== "status");
|
|
114365
|
+
d2 = observation.run;
|
|
114366
|
+
releaseObservation = observation.release;
|
|
114367
|
+
}
|
|
114368
|
+
}
|
|
114369
|
+
const observing = !!d2 && d2.callerId !== callerId;
|
|
114370
|
+
try {
|
|
114371
|
+
switch (opts.action) {
|
|
114372
|
+
case "status":
|
|
114373
|
+
if (d2 && !d2.settled) return this.agentWorking(d2, false, observing);
|
|
114374
|
+
return d2 ? await this.awaitAgent(key, d2, signal, observing) : this.agentAtRest(child);
|
|
114375
|
+
case "wait":
|
|
114376
|
+
return d2 ? await this.awaitAgent(key, d2, signal, observing) : this.agentAtRest(child);
|
|
114377
|
+
case "stop": {
|
|
114378
|
+
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.awaitingReport.delete(child.id);
|
|
114380
|
+
d2.stop("the session that started it stopped it", observing);
|
|
114381
|
+
await d2.outcome;
|
|
114382
|
+
if (!observing) this.delegations.delete(key);
|
|
114383
|
+
return { ok: true, state: "stopped", sessionId: child.id, sessionName: child.name, tool: child.tool };
|
|
114384
|
+
}
|
|
114385
|
+
case "delete": {
|
|
114386
|
+
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
|
+
const r = await this.cleanUpWorktreeChild(child);
|
|
114388
|
+
if (!r.ok) return r;
|
|
114389
|
+
if (d2) this.delegations.delete(key);
|
|
114390
|
+
return { ok: true, state: "deleted", sessionId: child.id, sessionName: child.name, tool: child.tool, branch: r.branch, setAside: r.setAside, ...r.warning ? { warning: r.warning } : {} };
|
|
113758
114391
|
}
|
|
113759
|
-
|
|
113760
|
-
|
|
113761
|
-
|
|
113762
|
-
|
|
113763
|
-
|
|
113764
|
-
|
|
113765
|
-
|
|
113766
|
-
|
|
113767
|
-
|
|
113768
|
-
|
|
113769
|
-
|
|
114392
|
+
case "send": {
|
|
114393
|
+
if (!presetWithin(child.permissionPreset, caller.permissionPreset)) {
|
|
114394
|
+
return {
|
|
114395
|
+
ok: false,
|
|
114396
|
+
error: `"${child.name}" now has more permissions (${child.permissionPreset}) than this session (${caller.permissionPreset}), so this session cannot give it work. Tell the person.`
|
|
114397
|
+
};
|
|
114398
|
+
}
|
|
114399
|
+
const from = { sessionId: callerId, name: labelOf(this.store, caller) };
|
|
114400
|
+
if (d2 && !d2.settled) {
|
|
114401
|
+
const text2 = agentNoteFrame(from.name, opts.text ?? "");
|
|
114402
|
+
if (!this.adapters.get(child.tool)?.capabilities.midTurnInput || !this.handles.get(child.id)?.sendTracked) {
|
|
114403
|
+
return { ok: false, error: `"${child.name}" cannot take messages while working. Wait for it (action "wait") or stop it first.` };
|
|
114404
|
+
}
|
|
114405
|
+
const releaseHold = d2.wait.holdInput();
|
|
114406
|
+
const release = () => {
|
|
114407
|
+
releaseHold();
|
|
114408
|
+
this.applyDeferredRestart(child.id);
|
|
114409
|
+
};
|
|
114410
|
+
try {
|
|
114411
|
+
await this.deliver(child.id, text2, void 0, from, false, void 0, "inline", (h, input) => {
|
|
114412
|
+
const fail4 = (e) => {
|
|
114413
|
+
d2.wait.fail(e);
|
|
114414
|
+
release();
|
|
114415
|
+
};
|
|
114416
|
+
void h.sendTracked(input, { complete: release, fail: fail4 }).then(release, fail4);
|
|
114417
|
+
});
|
|
114418
|
+
} catch (e) {
|
|
114419
|
+
release();
|
|
114420
|
+
return { ok: false, error: `Could not send it: ${e.message}` };
|
|
114421
|
+
}
|
|
114422
|
+
return this.agentWorking(d2, false, observing);
|
|
114423
|
+
}
|
|
114424
|
+
if (child.state === "working" || child.state === "waiting_approval" || this.agentRuns.has(child.id) && !this.agentRuns.get(child.id).finished) {
|
|
114425
|
+
return { ok: false, error: `"${child.name}" is busy with something else (the person may be using it). Try again later.` };
|
|
114426
|
+
}
|
|
114427
|
+
if (d2) this.delegations.delete(key);
|
|
114428
|
+
const full = this.agentCeilingError(callerId, child.projectId ?? "");
|
|
114429
|
+
if (full) return { ok: false, error: full };
|
|
114430
|
+
const project2 = this.store.listProjects().find((p) => p.id === child.projectId);
|
|
114431
|
+
const cwd = this.store.sessionCwd(child.id) ?? child.worktree?.path ?? project2?.path ?? "";
|
|
114432
|
+
const next = this.startDelegation(key, callerId, child.id, { id: child.projectId ?? "", name: project2?.name ?? "", path: cwd }, true);
|
|
114433
|
+
try {
|
|
114434
|
+
await this.deliver(child.id, agentFrame(from.name, opts.text ?? ""), void 0, from, false);
|
|
114435
|
+
} catch (e) {
|
|
114436
|
+
this.delegations.delete(key);
|
|
114437
|
+
if (this.agentRuns.get(child.id) === next.wait) this.agentRuns.delete(child.id);
|
|
114438
|
+
return { ok: false, error: `Could not send it: ${e.message}` };
|
|
114439
|
+
}
|
|
114440
|
+
return this.awaitAgent(key, next, signal);
|
|
113770
114441
|
}
|
|
113771
|
-
|
|
114442
|
+
default:
|
|
114443
|
+
return { ok: false, error: "Unknown agent_session action." };
|
|
113772
114444
|
}
|
|
114445
|
+
} finally {
|
|
114446
|
+
releaseObservation?.();
|
|
113773
114447
|
}
|
|
113774
114448
|
}
|
|
113775
114449
|
/**
|
|
@@ -113782,65 +114456,67 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113782
114456
|
if (!summary.isRepo) return { error: "This project is not a git repository, so it has no worktrees. Start the agent without one." };
|
|
113783
114457
|
const asked = opts.branch?.trim();
|
|
113784
114458
|
if (asked) return await gitValidBranchName(asked) ? { branch: asked } : { error: `Not a valid branch name: ${asked}` };
|
|
113785
|
-
return { branch: agentBranch(opts.task,
|
|
114459
|
+
return { branch: agentBranch(opts.task, randomUUID10()) };
|
|
113786
114460
|
}
|
|
113787
|
-
/**
|
|
113788
|
-
|
|
113789
|
-
|
|
113790
|
-
* changes, and the branch's current tip has landed on the trunk (by ancestry, or by a merged pull request whose head
|
|
113791
|
-
* is that tip). Not the cached merged badge: a commit could have landed since (TOCTOU). Every deletion outside that
|
|
113792
|
-
* proof stays the person's, in the delete dialog. The session goes to the trash like any other (an agent never
|
|
113793
|
-
* deletes one for good), the worktree and branch are removed, the tool's own conversation is kept, and the tip is
|
|
113794
|
-
* logged for reflog recovery.
|
|
113795
|
-
*/
|
|
114461
|
+
/** Prevent a send/resume from racing the snapshot and worktree removal. */
|
|
114462
|
+
worktreeCleanup = /* @__PURE__ */ new Set();
|
|
114463
|
+
/** #608: the parent decides; local work follows the session into the trash in every preset. */
|
|
113796
114464
|
async cleanUpWorktreeChild(target) {
|
|
113797
114465
|
if (!target.worktree?.base || !target.projectId) {
|
|
113798
114466
|
return { ok: false, error: `"${target.name}" works in no worktree of its own, so there is nothing to clean up \u2014 the person deletes a session` };
|
|
113799
114467
|
}
|
|
113800
|
-
if (target.state === "working" || target.state === "waiting_approval" || this.agentRuns.has(target.id)) {
|
|
113801
|
-
return { ok: false, error: `"${target.name}" is still working \u2014
|
|
113802
|
-
}
|
|
113803
|
-
const cwd = this.cwdOf(target.projectId);
|
|
113804
|
-
const { branch, path: path2, base: base2 } = target.worktree;
|
|
113805
|
-
const wt2 = await gitWorktreeDirty(path2, this.gitTrust(target.projectId)).catch(() => ({ dirty: true, changedFiles: -1 }));
|
|
113806
|
-
if (wt2.dirty) {
|
|
113807
|
-
const n = wt2.changedFiles >= 0 ? `${wt2.changedFiles} ` : "";
|
|
113808
|
-
return { ok: false, error: `There are ${n}uncommitted changes in "${target.name}" \u2014 have it commit (or discard) them, then delete again` };
|
|
114468
|
+
if (target.state === "working" || target.state === "waiting_approval" || this.agentRuns.has(target.id) || sessionBusy({ ...target, live: this.handles.has(target.id) })) {
|
|
114469
|
+
return { ok: false, error: `"${target.name}" is still working \u2014 stop it or wait first` };
|
|
113809
114470
|
}
|
|
113810
|
-
|
|
113811
|
-
|
|
113812
|
-
|
|
113813
|
-
|
|
113814
|
-
|
|
113815
|
-
const
|
|
113816
|
-
|
|
113817
|
-
|
|
113818
|
-
|
|
113819
|
-
|
|
113820
|
-
|
|
113821
|
-
|
|
113822
|
-
|
|
114471
|
+
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
|
+
if (this.worktreeCleanup.has(target.id)) return { ok: false, error: "This worktree is already being removed" };
|
|
114473
|
+
this.worktreeCleanup.add(target.id);
|
|
114474
|
+
const { branch, path: path2 } = target.worktree;
|
|
114475
|
+
try {
|
|
114476
|
+
const cwd = this.cwdOf(target.projectId);
|
|
114477
|
+
const handle = this.handles.get(target.id);
|
|
114478
|
+
if (handle) {
|
|
114479
|
+
await handle.dispose();
|
|
114480
|
+
this.handles.delete(target.id);
|
|
114481
|
+
this.running.delete(target.id);
|
|
114482
|
+
}
|
|
114483
|
+
const trunk = await gitRevParse(cwd, this.trunkOf(target.projectId) ?? "HEAD");
|
|
114484
|
+
const setAside = await saveWorktreeRecovery(target.id, path2, trunk ?? "missing-trunk", branch);
|
|
114485
|
+
if (setAside.changedFiles || setAside.commits) {
|
|
114486
|
+
this.onEvent({
|
|
114487
|
+
type: "notice",
|
|
114488
|
+
sessionId: target.id,
|
|
114489
|
+
level: "info",
|
|
114490
|
+
from: "Centralu",
|
|
114491
|
+
label: "worktree recovery",
|
|
114492
|
+
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
|
+
});
|
|
113823
114494
|
}
|
|
114495
|
+
await this.trashSession(target.id, false, false, setAside);
|
|
114496
|
+
try {
|
|
114497
|
+
await gitWorktreeRemove(cwd, path2, true, true);
|
|
114498
|
+
} catch (e) {
|
|
114499
|
+
return { ok: true, branch, setAside, warning: `The session is in the trash, but worktree ${path2} and branch ${branch} were left: ${e.message}. Recovery files are at ${worktreeRecoveryDir(target.id)}.` };
|
|
114500
|
+
}
|
|
114501
|
+
try {
|
|
114502
|
+
await gitBranchDelete(cwd, branch, this.gitTrust(target.projectId));
|
|
114503
|
+
} catch (e) {
|
|
114504
|
+
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
|
+
}
|
|
114506
|
+
return { ok: true, branch, setAside };
|
|
114507
|
+
} catch (e) {
|
|
114508
|
+
return { ok: false, error: `Could not finish removing "${branch}": ${e.message}. Any saved recovery files are at ${worktreeRecoveryDir(target.id)}.` };
|
|
114509
|
+
} finally {
|
|
114510
|
+
this.worktreeCleanup.delete(target.id);
|
|
113824
114511
|
}
|
|
113825
|
-
if (!proof) {
|
|
113826
|
-
return {
|
|
113827
|
-
ok: false,
|
|
113828
|
-
error: `Could not prove that "${branch}" has landed on the trunk \u2014 it is deleted only once a merge (or a merged PR) is confirmed. Discarding it without proof is the person's job, in the delete dialog`
|
|
113829
|
-
};
|
|
113830
|
-
}
|
|
113831
|
-
const tip = await gitRevParse(cwd, `refs/heads/${branch}`);
|
|
113832
|
-
await this.trashSession(target.id);
|
|
113833
|
-
await gitWorktreeRemove(cwd, path2, true).catch(() => {
|
|
113834
|
-
});
|
|
113835
|
-
await gitBranchDelete(cwd, branch, this.gitTrust(target.projectId)).catch(() => {
|
|
113836
|
-
});
|
|
113837
|
-
console.error(`[worktree] cleaned up ${branch} (tip ${tip?.slice(0, 8) ?? "?"}, proof: ${proof})`);
|
|
113838
|
-
return { ok: true, branch, proof };
|
|
113839
114512
|
}
|
|
113840
|
-
/**
|
|
113841
|
-
agentCeilingError(projectId) {
|
|
114513
|
+
/** A saved ceiling overrides the caller's preset default: auto unlimited, otherwise AGENT_CEILING. */
|
|
114514
|
+
agentCeilingError(callerId, projectId) {
|
|
114515
|
+
const configured = this.uiPreferences().agentCeiling;
|
|
114516
|
+
const ceiling = configured ?? (this.autoBoundaryOpen(callerId) ? 0 : AGENT_CEILING);
|
|
114517
|
+
if (ceiling === 0) return null;
|
|
113842
114518
|
const running = [...this.delegations.values()].filter((d2) => d2.agent && !d2.settled && this.meta.get(d2.sessionId)?.projectId === projectId).length;
|
|
113843
|
-
if (running <
|
|
114519
|
+
if (running < ceiling) return null;
|
|
113844
114520
|
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.`;
|
|
113845
114521
|
}
|
|
113846
114522
|
/**
|
|
@@ -113900,8 +114576,8 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113900
114576
|
* "Working": the caller is told when the turn ends (#561), through the report the orchestrator's reportBack uses, unless
|
|
113901
114577
|
* a call is waiting on it by then (`awaitAgent` takes the marker down while it waits)
|
|
113902
114578
|
*/
|
|
113903
|
-
agentWorking(d2, arm = true) {
|
|
113904
|
-
if (arm) this.awaitingReport.set(d2.sessionId, d2.callerId);
|
|
114579
|
+
agentWorking(d2, arm = true, observing = false) {
|
|
114580
|
+
if (arm && !observing) this.awaitingReport.set(d2.sessionId, d2.callerId);
|
|
113905
114581
|
const notice = d2.notice();
|
|
113906
114582
|
return {
|
|
113907
114583
|
ok: true,
|
|
@@ -113909,7 +114585,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113909
114585
|
sessionId: d2.sessionId,
|
|
113910
114586
|
sessionName: this.meta.get(d2.sessionId)?.name ?? d2.sessionId,
|
|
113911
114587
|
tool: this.meta.get(d2.sessionId)?.tool ?? "",
|
|
113912
|
-
reported: this.awaitingReport.get(d2.sessionId) === d2.callerId,
|
|
114588
|
+
reported: !observing && this.awaitingReport.get(d2.sessionId) === d2.callerId,
|
|
113913
114589
|
...notice ? { notice } : {}
|
|
113914
114590
|
};
|
|
113915
114591
|
}
|
|
@@ -113926,32 +114602,43 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113926
114602
|
};
|
|
113927
114603
|
}
|
|
113928
114604
|
/** Waits on a child's turn up to the bound (#561), like `awaitDelegation`, and reads its outcome for the caller */
|
|
113929
|
-
async awaitAgent(key, d2, signal) {
|
|
113930
|
-
if (this.awaitingReport.get(d2.sessionId) === d2.callerId) this.awaitingReport.delete(d2.sessionId);
|
|
114605
|
+
async awaitAgent(key, d2, signal, observing = false) {
|
|
114606
|
+
if (!observing && this.awaitingReport.get(d2.sessionId) === d2.callerId) this.awaitingReport.delete(d2.sessionId);
|
|
113931
114607
|
const name = () => this.meta.get(d2.sessionId)?.name ?? d2.sessionId;
|
|
113932
114608
|
let timer;
|
|
113933
114609
|
let onAbort;
|
|
113934
|
-
const bound = new Promise((
|
|
113935
|
-
const aborted2 = new Promise((
|
|
114610
|
+
const bound = new Promise((resolve12) => timer = setTimeout(() => resolve12("bound"), ASK_WAIT_MS));
|
|
114611
|
+
const aborted2 = new Promise((resolve12) => {
|
|
113936
114612
|
if (!signal) return;
|
|
113937
|
-
onAbort = () =>
|
|
114613
|
+
onAbort = () => resolve12("aborted");
|
|
113938
114614
|
if (signal.aborted) onAbort();
|
|
113939
114615
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
113940
114616
|
});
|
|
113941
|
-
d2.waiters = (d2.waiters ?? 0) + 1;
|
|
114617
|
+
if (!observing) d2.waiters = (d2.waiters ?? 0) + 1;
|
|
113942
114618
|
try {
|
|
113943
114619
|
const r = await Promise.race([d2.outcome, bound, aborted2]);
|
|
113944
|
-
d2.waiters
|
|
114620
|
+
if (!observing) d2.waiters = (d2.waiters ?? 1) - 1;
|
|
113945
114621
|
if (r === "aborted") {
|
|
114622
|
+
if (observing) return { ok: false, error: `The observation was cancelled; "${name()}" was left running.` };
|
|
113946
114623
|
d2.stop("the call that started it was cancelled");
|
|
113947
114624
|
await d2.outcome;
|
|
113948
114625
|
this.delegations.delete(key);
|
|
113949
114626
|
return { ok: false, error: `The call was cancelled, so "${name()}" was stopped.` };
|
|
113950
114627
|
}
|
|
113951
|
-
if (r === "bound") return this.agentWorking(d2);
|
|
113952
|
-
if (this.delegations.get(key) === d2) this.delegations.delete(key);
|
|
114628
|
+
if (r === "bound") return this.agentWorking(d2, !observing, observing);
|
|
114629
|
+
if (!observing && this.delegations.get(key) === d2) this.delegations.delete(key);
|
|
113953
114630
|
if (!r.ok) return { ok: false, error: `"${name()}" [${d2.sessionId}] did not finish: ${r.error}. Tell the person, or give it a clearer task.` };
|
|
113954
|
-
const answer = finalAnswer(this.store.loadMessages(d2.sessionId, 50));
|
|
114631
|
+
const answer = r.answer ?? finalAnswer(this.store.loadMessages(d2.sessionId, 50));
|
|
114632
|
+
if (observing) return {
|
|
114633
|
+
ok: true,
|
|
114634
|
+
state: "done",
|
|
114635
|
+
sessionId: d2.sessionId,
|
|
114636
|
+
sessionName: name(),
|
|
114637
|
+
tool: this.meta.get(d2.sessionId)?.tool ?? "",
|
|
114638
|
+
answer: clipAnswer(answer, name()),
|
|
114639
|
+
readable: [],
|
|
114640
|
+
outside: []
|
|
114641
|
+
};
|
|
113955
114642
|
const { granted, outside } = readableGrants(pathsIn(answer), d2.target.path);
|
|
113956
114643
|
if (granted.length) {
|
|
113957
114644
|
const set3 = this.readGrants.get(d2.callerId) ?? /* @__PURE__ */ new Set();
|
|
@@ -113974,39 +114661,39 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
113974
114661
|
}
|
|
113975
114662
|
}
|
|
113976
114663
|
askCapability(q2, signal) {
|
|
113977
|
-
return new Promise((
|
|
113978
|
-
if (signal.aborted) return
|
|
114664
|
+
return new Promise((resolve12) => {
|
|
114665
|
+
if (signal.aborted) return resolve12(null);
|
|
113979
114666
|
const app2 = { appId: q2.app.appId, projectId: q2.app.projectId, name: q2.appName };
|
|
113980
114667
|
if (q2.origin.kind === "session" && this.meta.has(q2.origin.sessionId)) {
|
|
113981
|
-
const requestId = `cap-${
|
|
114668
|
+
const requestId = `cap-${randomUUID10()}`;
|
|
113982
114669
|
const ask = {
|
|
113983
114670
|
requestId,
|
|
113984
114671
|
sessionId: q2.origin.sessionId,
|
|
113985
114672
|
detail: { kind: "capability", app: app2, capability: q2.capability, text: q2.text },
|
|
113986
114673
|
shown: false,
|
|
113987
|
-
resolve: (d2) =>
|
|
114674
|
+
resolve: (d2) => resolve12(d2 === "always" || d2 === "always_project" ? "allow" : d2)
|
|
113988
114675
|
};
|
|
113989
114676
|
this.hostAsks.set(requestId, ask);
|
|
113990
114677
|
signal.addEventListener("abort", () => {
|
|
113991
114678
|
if (!this.hostAsks.delete(requestId)) return;
|
|
113992
114679
|
const m = this.meta.get(ask.sessionId);
|
|
113993
114680
|
if (m?.pendingApproval?.requestId === requestId) this.onEvent({ type: "approval_resolved", sessionId: ask.sessionId, requestId, decision: "deny" });
|
|
113994
|
-
|
|
114681
|
+
resolve12(null);
|
|
113995
114682
|
}, { once: true });
|
|
113996
114683
|
this.raiseHostAsks(ask.sessionId);
|
|
113997
114684
|
return;
|
|
113998
114685
|
}
|
|
113999
114686
|
const origin = q2.origin.kind === "view" ? q2.origin.app : q2.app;
|
|
114000
|
-
const id2 = `q-${
|
|
114687
|
+
const id2 = `q-${randomUUID10()}`;
|
|
114001
114688
|
this.appQuestions.set(id2, {
|
|
114002
114689
|
question: { id: id2, app: app2, capability: q2.capability, text: q2.text, origin: { appId: origin.appId, projectId: origin.projectId }, askedAt: q2.askedAt, expiresAt: q2.expiresAt },
|
|
114003
|
-
resolve:
|
|
114690
|
+
resolve: resolve12
|
|
114004
114691
|
});
|
|
114005
114692
|
this.emit({ type: "external_app_questions_changed" });
|
|
114006
114693
|
signal.addEventListener("abort", () => {
|
|
114007
114694
|
if (!this.appQuestions.delete(id2)) return;
|
|
114008
114695
|
this.emit({ type: "external_app_questions_changed" });
|
|
114009
|
-
|
|
114696
|
+
resolve12(null);
|
|
114010
114697
|
}, { once: true });
|
|
114011
114698
|
});
|
|
114012
114699
|
}
|
|
@@ -114315,6 +115002,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
114315
115002
|
this.appsHub?.dispose();
|
|
114316
115003
|
this.appsHub = new SessionAppsHub(rt2, {
|
|
114317
115004
|
...opts,
|
|
115005
|
+
boundaryOpen: (id2) => this.autoBoundaryOpen(id2),
|
|
114318
115006
|
onDemand: {
|
|
114319
115007
|
attached: (id2) => this.appAccess?.attached(id2) ?? [],
|
|
114320
115008
|
/*
|
|
@@ -114325,7 +115013,9 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
114325
115013
|
allowed: (session2, app2) => this.toolProfileOf(session2.id) === "reader" && (this.appAccess?.allowed(session2, app2) ?? false)
|
|
114326
115014
|
}
|
|
114327
115015
|
});
|
|
115016
|
+
rt2.setSessionBoundaryPolicy((id2) => this.autoBoundaryOpen(id2));
|
|
114328
115017
|
this.appAccess = new AppAccess({
|
|
115018
|
+
boundaryOpen: (id2, boundary) => this.autoBoundaryOpen(id2, boundary),
|
|
114329
115019
|
hub: this.appsHub,
|
|
114330
115020
|
store: this.store,
|
|
114331
115021
|
session: (id2) => {
|
|
@@ -114679,14 +115369,14 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
114679
115369
|
}
|
|
114680
115370
|
async loadMessages(sessionId, limit, beforeSeq) {
|
|
114681
115371
|
const rows = this.store.loadMessages(sessionId, limit, beforeSeq);
|
|
114682
|
-
const { readFile:
|
|
115372
|
+
const { readFile: readFile6 } = await import("node:fs/promises");
|
|
114683
115373
|
return Promise.all(
|
|
114684
115374
|
rows.map(async (r) => {
|
|
114685
115375
|
if (r.kind === "image") {
|
|
114686
115376
|
const p = r.payload;
|
|
114687
115377
|
if (!p.path || p.note) return r;
|
|
114688
115378
|
try {
|
|
114689
|
-
const data = (await
|
|
115379
|
+
const data = (await readFile6(p.path)).toString("base64");
|
|
114690
115380
|
return { ...r, payload: { ...p, data } };
|
|
114691
115381
|
} catch {
|
|
114692
115382
|
return { ...r, payload: { ...p, note: "The image was cleared and no longer exists (total-size cap)" } };
|
|
@@ -114699,7 +115389,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
114699
115389
|
p.attachments.map(async (a) => {
|
|
114700
115390
|
if (a.kind !== "image") return a;
|
|
114701
115391
|
try {
|
|
114702
|
-
return { ...a, data: (await
|
|
115392
|
+
return { ...a, data: (await readFile6(a.path)).toString("base64") };
|
|
114703
115393
|
} catch {
|
|
114704
115394
|
return a;
|
|
114705
115395
|
}
|
|
@@ -114713,6 +115403,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
114713
115403
|
}
|
|
114714
115404
|
async disposeAll() {
|
|
114715
115405
|
this.watchers.close();
|
|
115406
|
+
for (const readers of this.turnObservers.values()) for (const observer of readers) observer.fail(new Error("Centralu is stopping"));
|
|
114716
115407
|
this.appsHub?.rt.attachBrokerHost(null);
|
|
114717
115408
|
for (const run of [...this.agentRuns.values()]) run.fail(new Error("Centralu is shutting down"));
|
|
114718
115409
|
for (const ask of [...this.hostAsks.values()]) ask.resolve(null);
|
|
@@ -114743,6 +115434,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
114743
115434
|
this.watchers.close();
|
|
114744
115435
|
this.appsHub?.rt.attachBrokerHost(null);
|
|
114745
115436
|
for (const run of [...this.agentRuns.values()]) run.fail(new Error("Centralu is restarting"));
|
|
115437
|
+
for (const readers of this.turnObservers.values()) for (const observer of readers) observer.fail(new Error("Centralu is restarting"));
|
|
114746
115438
|
for (const ask of [...this.hostAsks.values()]) ask.resolve(null);
|
|
114747
115439
|
this.hostAsks.clear();
|
|
114748
115440
|
for (const q2 of [...this.appQuestions.values()]) q2.resolve(null);
|
|
@@ -114774,8 +115466,8 @@ import { relative as relative9 } from "node:path";
|
|
|
114774
115466
|
// packages/agent-host/src/themes.ts
|
|
114775
115467
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
114776
115468
|
import { watch as watch2 } from "node:fs";
|
|
114777
|
-
import { mkdir as
|
|
114778
|
-
import { basename as basename6, join as
|
|
115469
|
+
import { mkdir as mkdir7, readdir as readdir9, readFile as readFile5, rename as rename4, stat as stat6, unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
|
|
115470
|
+
import { basename as basename6, join as join30 } from "node:path";
|
|
114779
115471
|
var MAX_FILE_BYTES = 256 * 1024;
|
|
114780
115472
|
var SETTLE_MS = 100;
|
|
114781
115473
|
var ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,59}$/;
|
|
@@ -114790,7 +115482,7 @@ var ThemeFiles = class {
|
|
|
114790
115482
|
timer = null;
|
|
114791
115483
|
/** Makes the folder, writes the schema next to the themes, and starts watching */
|
|
114792
115484
|
async start() {
|
|
114793
|
-
await
|
|
115485
|
+
await mkdir7(this.dir, { recursive: true });
|
|
114794
115486
|
await this.writeSchema();
|
|
114795
115487
|
try {
|
|
114796
115488
|
this.watcher = watch2(this.dir, (_event, name) => {
|
|
@@ -114820,15 +115512,15 @@ var ThemeFiles = class {
|
|
|
114820
115512
|
* a token updates it, and an unchanged one does not touch the folder (or wake the watcher).
|
|
114821
115513
|
*/
|
|
114822
115514
|
async writeSchema() {
|
|
114823
|
-
const path2 =
|
|
115515
|
+
const path2 = join30(this.dir, THEME_SCHEMA_FILE);
|
|
114824
115516
|
const text2 = `${JSON.stringify(themeFileJsonSchema(), null, 2)}
|
|
114825
115517
|
`;
|
|
114826
|
-
const current = await
|
|
115518
|
+
const current = await readFile5(path2, "utf8").catch(() => null);
|
|
114827
115519
|
if (current !== text2) await this.atomicWrite(path2, text2);
|
|
114828
115520
|
}
|
|
114829
115521
|
/** Every `<id>.json` in the folder, read and checked. A file that cannot be read is listed with the reason. */
|
|
114830
115522
|
async list() {
|
|
114831
|
-
const names = await
|
|
115523
|
+
const names = await readdir9(this.dir).catch(() => []);
|
|
114832
115524
|
const entries = [];
|
|
114833
115525
|
const ids = names.filter((name) => name.endsWith(".json") && name !== THEME_SCHEMA_FILE).map((name) => name.slice(0, -".json".length)).filter((id2) => ID_PATTERN.test(id2)).sort();
|
|
114834
115526
|
for (const id2 of ids) entries.push(await this.read(id2));
|
|
@@ -114837,11 +115529,11 @@ var ThemeFiles = class {
|
|
|
114837
115529
|
async read(id2) {
|
|
114838
115530
|
const path2 = this.pathOf(id2);
|
|
114839
115531
|
try {
|
|
114840
|
-
const info = await
|
|
115532
|
+
const info = await stat6(path2);
|
|
114841
115533
|
if (info.size > MAX_FILE_BYTES) {
|
|
114842
115534
|
return { id: id2, path: path2, name: id2, base: "dark", tokens: {}, problems: ["The file is too large to be a theme."], broken: true };
|
|
114843
115535
|
}
|
|
114844
|
-
return parseThemeFile(id2, path2, await
|
|
115536
|
+
return parseThemeFile(id2, path2, await readFile5(path2, "utf8"));
|
|
114845
115537
|
} catch (e) {
|
|
114846
115538
|
return { id: id2, path: path2, name: id2, base: "dark", tokens: {}, problems: [`Could not read the file: ${e.message}`], broken: true };
|
|
114847
115539
|
}
|
|
@@ -114849,16 +115541,16 @@ var ThemeFiles = class {
|
|
|
114849
115541
|
/** Writes a theme. `id` null makes a new file named after the theme (`My theme` → `my-theme.json`). */
|
|
114850
115542
|
async save(id2, content) {
|
|
114851
115543
|
const target = id2 ?? await this.freeId(content.name);
|
|
114852
|
-
await
|
|
115544
|
+
await mkdir7(this.dir, { recursive: true });
|
|
114853
115545
|
await this.atomicWrite(this.pathOf(target), formatThemeFile(content));
|
|
114854
115546
|
return this.read(target);
|
|
114855
115547
|
}
|
|
114856
115548
|
/** Copies a file from elsewhere in, under a fresh id if its name is taken. Refuses what does not read as a theme. */
|
|
114857
115549
|
async importFrom(source) {
|
|
114858
|
-
const info = await
|
|
115550
|
+
const info = await stat6(source);
|
|
114859
115551
|
if (!info.isFile()) throw Object.assign(new Error("That is not a file."), { code: "invalid_params" });
|
|
114860
115552
|
if (info.size > MAX_FILE_BYTES) throw Object.assign(new Error("The file is too large to be a theme."), { code: "invalid_params" });
|
|
114861
|
-
const text2 = await
|
|
115553
|
+
const text2 = await readFile5(source, "utf8");
|
|
114862
115554
|
const parsed = parseThemeFile("import", source, text2);
|
|
114863
115555
|
if (parsed.broken) throw Object.assign(new Error(parsed.problems[0] ?? "Not a theme file."), { code: "invalid_params" });
|
|
114864
115556
|
const fromName = basename6(source).replace(/\.json$/i, "");
|
|
@@ -114869,13 +115561,13 @@ var ThemeFiles = class {
|
|
|
114869
115561
|
/** The absolute path for an id, for the shell to trash or reveal. Refuses anything that is not an id. */
|
|
114870
115562
|
pathOf(id2) {
|
|
114871
115563
|
if (!ID_PATTERN.test(id2)) throw Object.assign(new Error(`Not a theme id: ${id2}`), { code: "invalid_params" });
|
|
114872
|
-
return
|
|
115564
|
+
return join30(this.dir, `${id2}.json`);
|
|
114873
115565
|
}
|
|
114874
115566
|
async freeId(name) {
|
|
114875
115567
|
const base2 = name.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "theme";
|
|
114876
115568
|
for (let n = 1; n < 1e3; n++) {
|
|
114877
115569
|
const id2 = n === 1 ? base2 : `${base2}-${n}`;
|
|
114878
|
-
const taken = await
|
|
115570
|
+
const taken = await stat6(this.pathOf(id2)).then(
|
|
114879
115571
|
() => true,
|
|
114880
115572
|
() => false
|
|
114881
115573
|
);
|
|
@@ -114887,7 +115579,7 @@ var ThemeFiles = class {
|
|
|
114887
115579
|
async atomicWrite(path2, text2) {
|
|
114888
115580
|
const tmp = `${path2}.${randomBytes4(6).toString("hex")}.tmp`;
|
|
114889
115581
|
try {
|
|
114890
|
-
await
|
|
115582
|
+
await writeFile5(tmp, text2, "utf8");
|
|
114891
115583
|
await rename4(tmp, path2);
|
|
114892
115584
|
} catch (e) {
|
|
114893
115585
|
await unlink2(tmp).catch(() => {
|
|
@@ -115032,13 +115724,24 @@ async function stopStrays(pids, roots, selfPid = process.pid, held3 = []) {
|
|
|
115032
115724
|
}
|
|
115033
115725
|
|
|
115034
115726
|
// packages/agent-host/src/app-home-view.ts
|
|
115035
|
-
async function openHomeView(apps, views2, ref, holder) {
|
|
115727
|
+
async function openHomeView(apps, views2, ref, holder, openKey) {
|
|
115728
|
+
const prior = openKey ? views2.homeOpen(ref, openKey, holder) : null;
|
|
115729
|
+
if (prior) return prior;
|
|
115036
115730
|
const home = await apps.homeView(ref);
|
|
115731
|
+
const concurrent = openKey ? views2.homeOpen(ref, openKey, holder) : null;
|
|
115732
|
+
if (concurrent) return concurrent;
|
|
115037
115733
|
const toolInput = {};
|
|
115038
|
-
const
|
|
115039
|
-
|
|
115040
|
-
|
|
115041
|
-
|
|
115734
|
+
const invoke = async () => {
|
|
115735
|
+
const out = await apps.call(ref, home.tool, toolInput, { kind: "view" });
|
|
115736
|
+
if (!out.result) throw Object.assign(new Error(out.error ?? `The home tool "${home.tool}" did not answer`), { code: "internal" });
|
|
115737
|
+
return { tool: home.tool, resourceUri: home.resourceUri, toolInput, toolResult: out.result, runId: out.runId };
|
|
115738
|
+
};
|
|
115739
|
+
if (openKey) {
|
|
115740
|
+
const { instanceId } = views2.open(ref, home.resourceUri, holder);
|
|
115741
|
+
return views2.rememberHome(instanceId, openKey, async () => ({ instanceId, ...await invoke() }));
|
|
115742
|
+
}
|
|
115743
|
+
const outcome = await invoke();
|
|
115744
|
+
return { ...views2.open(ref, home.resourceUri, holder), ...outcome };
|
|
115042
115745
|
}
|
|
115043
115746
|
|
|
115044
115747
|
// packages/agent-host/src/builder-requests.ts
|
|
@@ -115351,8 +116054,14 @@ function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: upd
|
|
|
115351
116054
|
return { appId: d2.app.appId, projectId: d2.app.projectId, uri: d2.uri, origin: d2.origin, resource: d2.resource };
|
|
115352
116055
|
},
|
|
115353
116056
|
"apps.openView": async (p, ctx) => {
|
|
115354
|
-
const { appId, projectId, lease } = RpcMethods["apps.openView"].params.parse(p);
|
|
115355
|
-
return openHomeView(requireExternalApps(), requireViews(), { appId, projectId }, lease && ctx ? ctx.client : void 0);
|
|
116057
|
+
const { appId, projectId, lease, openKey } = RpcMethods["apps.openView"].params.parse(p);
|
|
116058
|
+
return openHomeView(requireExternalApps(), requireViews(), { appId, projectId }, lease && ctx ? ctx.client : void 0, openKey);
|
|
116059
|
+
},
|
|
116060
|
+
"apps.recoverView": async (p, ctx) => {
|
|
116061
|
+
const { appId, projectId, openKey, lease } = RpcMethods["apps.recoverView"].params.parse(p);
|
|
116062
|
+
const prior = requireViews().homeOpen({ appId, projectId }, openKey, lease && ctx ? ctx.client : void 0);
|
|
116063
|
+
if (prior) return prior;
|
|
116064
|
+
throw new Error("The original app open could not be recovered \u2014 the home tool may or may not have run; check before opening again");
|
|
115356
116065
|
},
|
|
115357
116066
|
"apps.holdViews": async (p, ctx) => {
|
|
115358
116067
|
const { instanceIds } = RpcMethods["apps.holdViews"].params.parse(p);
|
|
@@ -115891,7 +116600,7 @@ var sameCause = (a, b2) => a !== null && b2 !== null && JSON.stringify(a) === JS
|
|
|
115891
116600
|
|
|
115892
116601
|
// packages/agent-host/src/dev-services/terminal.ts
|
|
115893
116602
|
import { createRequire } from "node:module";
|
|
115894
|
-
import { existsSync as
|
|
116603
|
+
import { existsSync as existsSync20 } from "node:fs";
|
|
115895
116604
|
import { win32 as win327 } from "node:path";
|
|
115896
116605
|
var require2 = createRequire(import.meta.url);
|
|
115897
116606
|
var SCROLLBACK_BYTES = 256 * 1024;
|
|
@@ -116123,9 +116832,9 @@ function commandShell(command, platform = process.platform, env = process.env) {
|
|
|
116123
116832
|
}
|
|
116124
116833
|
function shellPath() {
|
|
116125
116834
|
const fromEnv = process.env.SHELL;
|
|
116126
|
-
if (fromEnv &&
|
|
116835
|
+
if (fromEnv && existsSync20(fromEnv)) return fromEnv;
|
|
116127
116836
|
for (const candidate of ["/bin/zsh", "/bin/bash", "/bin/sh"]) {
|
|
116128
|
-
if (
|
|
116837
|
+
if (existsSync20(candidate)) return candidate;
|
|
116129
116838
|
}
|
|
116130
116839
|
return process.platform === "win32" ? "powershell.exe" : "/bin/sh";
|
|
116131
116840
|
}
|
|
@@ -116312,7 +117021,7 @@ var CommandRunner = class {
|
|
|
116312
117021
|
|
|
116313
117022
|
// packages/agent-host/src/updates.ts
|
|
116314
117023
|
import { execFile as execFile8 } from "node:child_process";
|
|
116315
|
-
import { existsSync as
|
|
117024
|
+
import { existsSync as existsSync21 } from "node:fs";
|
|
116316
117025
|
var REGISTRY_URL = `https://registry.npmjs.org/${APP_SLUG}/latest`;
|
|
116317
117026
|
var CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
116318
117027
|
var SCHEDULED_TIMEOUT_MS = 3e3;
|
|
@@ -116322,7 +117031,7 @@ var NPM_RETRY_DELAY_MS = 3e4;
|
|
|
116322
117031
|
var NPM_RETRY_WINDOW_MS = 10 * 60 * 1e3;
|
|
116323
117032
|
function existingInstalledCopy() {
|
|
116324
117033
|
const path2 = installedCopyPath();
|
|
116325
|
-
return
|
|
117034
|
+
return existsSync21(path2) ? path2 : null;
|
|
116326
117035
|
}
|
|
116327
117036
|
async function fetchLatestFromRegistry(timeoutMs) {
|
|
116328
117037
|
try {
|
|
@@ -116336,16 +117045,16 @@ async function fetchLatestFromRegistry(timeoutMs) {
|
|
|
116336
117045
|
return { ok: false, reason: "Could not reach the registry \u2014 check the network" };
|
|
116337
117046
|
}
|
|
116338
117047
|
}
|
|
116339
|
-
function commandFor(file2, args,
|
|
116340
|
-
const launch2 =
|
|
117048
|
+
function commandFor(file2, args, resolve12 = (f) => resolveCommand(f)) {
|
|
117049
|
+
const launch2 = resolve12(file2);
|
|
116341
117050
|
return { file: launch2.command, args: [...launch2.args, ...args] };
|
|
116342
117051
|
}
|
|
116343
117052
|
function failureDetail(file2, stderr) {
|
|
116344
117053
|
const lines = stderr.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !/complete log of this run/i.test(l));
|
|
116345
117054
|
return (file2 === "npm" ? lines.at(-1) : lines[0]) ?? null;
|
|
116346
117055
|
}
|
|
116347
|
-
function runCommand(file2, args,
|
|
116348
|
-
const cmd = commandFor(file2, args,
|
|
117056
|
+
function runCommand(file2, args, resolve12) {
|
|
117057
|
+
const cmd = commandFor(file2, args, resolve12);
|
|
116349
117058
|
return new Promise((done, reject) => {
|
|
116350
117059
|
execFile8(cmd.file, cmd.args, { timeout: INSTALL_TIMEOUT_MS, windowsHide: true }, (err, _stdout, stderr) => {
|
|
116351
117060
|
if (!err) return done();
|
|
@@ -116373,7 +117082,7 @@ var UpdateService = class {
|
|
|
116373
117082
|
readAutoApply: deps.readAutoApply ?? (() => false),
|
|
116374
117083
|
writeAutoApply: deps.writeAutoApply ?? (() => {
|
|
116375
117084
|
}),
|
|
116376
|
-
wait: deps.wait ?? ((ms3) => new Promise((
|
|
117085
|
+
wait: deps.wait ?? ((ms3) => new Promise((resolve12) => setTimeout(resolve12, ms3)))
|
|
116377
117086
|
};
|
|
116378
117087
|
this.status = {
|
|
116379
117088
|
/*
|
|
@@ -116919,7 +117628,7 @@ var SessionSleeper = class {
|
|
|
116919
117628
|
// packages/agent-host/src/dev-services/instance-lock.ts
|
|
116920
117629
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
116921
117630
|
import { readFileSync as readFileSync13, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7 } from "node:fs";
|
|
116922
|
-
import { dirname as dirname10, join as
|
|
117631
|
+
import { dirname as dirname10, join as join31 } from "node:path";
|
|
116923
117632
|
import Database2 from "better-sqlite3";
|
|
116924
117633
|
var OWNERSHIP_FILE = "host-ownership.sqlite";
|
|
116925
117634
|
var held = /* @__PURE__ */ new Set();
|
|
@@ -116953,7 +117662,7 @@ function parseHolder(raw) {
|
|
|
116953
117662
|
function acquireInstanceLock(dbPath2, startOf = processStartTime) {
|
|
116954
117663
|
if (dbPath2 === ":memory:") return { ok: true, release: () => {
|
|
116955
117664
|
} };
|
|
116956
|
-
const lockPath =
|
|
117665
|
+
const lockPath = join31(dirname10(dbPath2), "host.lock");
|
|
116957
117666
|
const readHolder = () => {
|
|
116958
117667
|
try {
|
|
116959
117668
|
return parseHolder(readFileSync13(lockPath, "utf8"));
|
|
@@ -116961,7 +117670,7 @@ function acquireInstanceLock(dbPath2, startOf = processStartTime) {
|
|
|
116961
117670
|
return null;
|
|
116962
117671
|
}
|
|
116963
117672
|
};
|
|
116964
|
-
const owner = takeOwnership(
|
|
117673
|
+
const owner = takeOwnership(join31(dirname10(dbPath2), OWNERSHIP_FILE));
|
|
116965
117674
|
if (!owner) {
|
|
116966
117675
|
const holder = readHolder();
|
|
116967
117676
|
return { ok: false, heldByPid: holder && Number.isInteger(holder.pid) && holder.pid > 0 ? holder.pid : null, lockPath };
|
|
@@ -117282,9 +117991,9 @@ var ControlChannel = class {
|
|
|
117282
117991
|
}
|
|
117283
117992
|
next(op) {
|
|
117284
117993
|
if (this.ended) return Promise.reject(new Error("the keeper closed the control channel"));
|
|
117285
|
-
return new Promise((
|
|
117994
|
+
return new Promise((resolve12, reject) => {
|
|
117286
117995
|
const list = this.waiters.get(op) ?? [];
|
|
117287
|
-
list.push({ resolve:
|
|
117996
|
+
list.push({ resolve: resolve12, reject });
|
|
117288
117997
|
this.waiters.set(op, list);
|
|
117289
117998
|
});
|
|
117290
117999
|
}
|
|
@@ -117389,7 +118098,7 @@ async function runDrain(bound, deps) {
|
|
|
117389
118098
|
}
|
|
117390
118099
|
|
|
117391
118100
|
// packages/agent-host/src/links/install.ts
|
|
117392
|
-
import { existsSync as
|
|
118101
|
+
import { existsSync as existsSync22, readFileSync as readFileSync14 } from "node:fs";
|
|
117393
118102
|
import { hostname as hostname3 } from "node:os";
|
|
117394
118103
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
117395
118104
|
import { gzipSync } from "node:zlib";
|
|
@@ -117713,7 +118422,7 @@ var SshTunnel = class {
|
|
|
117713
118422
|
if (this.closing) throw new Error(`The link to ${this.opts.target} was closed`);
|
|
117714
118423
|
}
|
|
117715
118424
|
run(args, stdin) {
|
|
117716
|
-
return new Promise((
|
|
118425
|
+
return new Promise((resolve12, reject) => {
|
|
117717
118426
|
const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: [stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"] });
|
|
117718
118427
|
if (stdin !== void 0) {
|
|
117719
118428
|
child.stdin.on("error", () => {
|
|
@@ -117730,7 +118439,7 @@ var SshTunnel = class {
|
|
|
117730
118439
|
this.asking.delete(child);
|
|
117731
118440
|
reject(e);
|
|
117732
118441
|
});
|
|
117733
|
-
child.once("close", (code2) =>
|
|
118442
|
+
child.once("close", (code2) => resolve12({ code: code2, stdout, stderr }));
|
|
117734
118443
|
});
|
|
117735
118444
|
}
|
|
117736
118445
|
config() {
|
|
@@ -117777,7 +118486,7 @@ var SshTunnel = class {
|
|
|
117777
118486
|
this.child = child;
|
|
117778
118487
|
let stderr = "";
|
|
117779
118488
|
child.stderr.on("data", (d2) => stderr += String(d2));
|
|
117780
|
-
return new Promise((
|
|
118489
|
+
return new Promise((resolve12, reject) => {
|
|
117781
118490
|
let settled = false;
|
|
117782
118491
|
const deadline = Date.now() + (this.opts.forwardTimeoutMs ?? 2e4);
|
|
117783
118492
|
const fail4 = (message) => {
|
|
@@ -117796,7 +118505,7 @@ var SshTunnel = class {
|
|
|
117796
118505
|
if (await accepts(localPort)) {
|
|
117797
118506
|
settled = true;
|
|
117798
118507
|
this.opts.log?.(`[links] forward up: 127.0.0.1:${localPort} -> ${this.opts.target}:127.0.0.1:${remotePort}`);
|
|
117799
|
-
return
|
|
118508
|
+
return resolve12();
|
|
117800
118509
|
}
|
|
117801
118510
|
if (Date.now() > deadline) {
|
|
117802
118511
|
this.ended.add(child);
|
|
@@ -117814,12 +118523,12 @@ var SshTunnel = class {
|
|
|
117814
118523
|
this.child = null;
|
|
117815
118524
|
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
117816
118525
|
this.ended.add(child);
|
|
117817
|
-
await new Promise((
|
|
117818
|
-
child.once("exit", () =>
|
|
118526
|
+
await new Promise((resolve12) => {
|
|
118527
|
+
child.once("exit", () => resolve12());
|
|
117819
118528
|
child.kill("SIGTERM");
|
|
117820
118529
|
setTimeout(() => {
|
|
117821
118530
|
child.kill("SIGKILL");
|
|
117822
|
-
|
|
118531
|
+
resolve12();
|
|
117823
118532
|
}, 2e3).unref();
|
|
117824
118533
|
});
|
|
117825
118534
|
}
|
|
@@ -117829,29 +118538,29 @@ function lastLine(s) {
|
|
|
117829
118538
|
return [...lines].reverse().find((l) => /error|denied|refused|timed out|not found|cannot|could not/i.test(l)) ?? lines.at(-1) ?? "";
|
|
117830
118539
|
}
|
|
117831
118540
|
function accepts(port2) {
|
|
117832
|
-
return new Promise((
|
|
118541
|
+
return new Promise((resolve12) => {
|
|
117833
118542
|
const s = createConnection2({ host: "127.0.0.1", port: port2 });
|
|
117834
118543
|
s.once("connect", () => {
|
|
117835
118544
|
s.destroy();
|
|
117836
|
-
|
|
118545
|
+
resolve12(true);
|
|
117837
118546
|
});
|
|
117838
|
-
s.once("error", () =>
|
|
118547
|
+
s.once("error", () => resolve12(false));
|
|
117839
118548
|
});
|
|
117840
118549
|
}
|
|
117841
118550
|
function portFree(port2) {
|
|
117842
|
-
return new Promise((
|
|
118551
|
+
return new Promise((resolve12) => {
|
|
117843
118552
|
const probe = createServer3();
|
|
117844
|
-
probe.once("error", () =>
|
|
117845
|
-
probe.listen(port2, "127.0.0.1", () => probe.close(() =>
|
|
118553
|
+
probe.once("error", () => resolve12(false));
|
|
118554
|
+
probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve12(true)));
|
|
117846
118555
|
});
|
|
117847
118556
|
}
|
|
117848
118557
|
function freePort() {
|
|
117849
|
-
return new Promise((
|
|
118558
|
+
return new Promise((resolve12, reject) => {
|
|
117850
118559
|
const probe = createServer3();
|
|
117851
118560
|
probe.once("error", reject);
|
|
117852
118561
|
probe.listen(0, "127.0.0.1", () => {
|
|
117853
118562
|
const { port: port2 } = probe.address();
|
|
117854
|
-
probe.close(() =>
|
|
118563
|
+
probe.close(() => resolve12(port2));
|
|
117855
118564
|
});
|
|
117856
118565
|
});
|
|
117857
118566
|
}
|
|
@@ -118067,7 +118776,7 @@ async function installRemote(o) {
|
|
|
118067
118776
|
function bundled(name, source) {
|
|
118068
118777
|
for (const url2 of [new URL(`./${name}`, import.meta.url), new URL(source, import.meta.url)]) {
|
|
118069
118778
|
const file2 = fileURLToPath6(url2);
|
|
118070
|
-
if (
|
|
118779
|
+
if (existsSync22(file2)) return readFileSync14(file2, "utf8");
|
|
118071
118780
|
}
|
|
118072
118781
|
throw new Error(`${name} is missing from this host`);
|
|
118073
118782
|
}
|
|
@@ -118170,7 +118879,7 @@ function probeTarget(target, opts = {}) {
|
|
|
118170
118879
|
return Promise.reject(Object.assign(new Error("Not an ssh target: use a host alias from ~/.ssh/config, or user@host"), { code: "internal" }));
|
|
118171
118880
|
}
|
|
118172
118881
|
const args = [...opts.configFile ? ["-F", opts.configFile] : [], "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "--", t, probeCommand()];
|
|
118173
|
-
return new Promise((
|
|
118882
|
+
return new Promise((resolve12) => {
|
|
118174
118883
|
let stdout = "";
|
|
118175
118884
|
let stderr = "";
|
|
118176
118885
|
let done = false;
|
|
@@ -118178,7 +118887,7 @@ function probeTarget(target, opts = {}) {
|
|
|
118178
118887
|
if (done) return;
|
|
118179
118888
|
done = true;
|
|
118180
118889
|
clearTimeout(timer);
|
|
118181
|
-
|
|
118890
|
+
resolve12(parseProbe(t, run));
|
|
118182
118891
|
};
|
|
118183
118892
|
const child = spawn6(opts.ssh ?? "ssh", args, { env: opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
118184
118893
|
const timer = setTimeout(() => {
|
|
@@ -118196,7 +118905,7 @@ function probeTarget(target, opts = {}) {
|
|
|
118196
118905
|
import { spawn as spawn7 } from "node:child_process";
|
|
118197
118906
|
import { readdirSync as readdirSync8, readFileSync as readFileSync15, statSync as statSync13 } from "node:fs";
|
|
118198
118907
|
import { homedir as homedir13 } from "node:os";
|
|
118199
|
-
import { isAbsolute as isAbsolute10, join as
|
|
118908
|
+
import { isAbsolute as isAbsolute10, join as join32, normalize, parse as parse6, sep as sep12 } from "node:path";
|
|
118200
118909
|
var MAX_DEPTH2 = 16;
|
|
118201
118910
|
function splitConfigLine(raw) {
|
|
118202
118911
|
const m = /^\s*([^\s=#]+)\s*(?:=\s*)?([\s\S]*)$/.exec(raw);
|
|
@@ -118262,7 +118971,7 @@ function expandGlob(path2) {
|
|
|
118262
118971
|
const next = [];
|
|
118263
118972
|
for (const base2 of found) {
|
|
118264
118973
|
if (!MAGIC.test(seg)) {
|
|
118265
|
-
next.push(
|
|
118974
|
+
next.push(join32(base2, seg));
|
|
118266
118975
|
continue;
|
|
118267
118976
|
}
|
|
118268
118977
|
let names;
|
|
@@ -118274,7 +118983,7 @@ function expandGlob(path2) {
|
|
|
118274
118983
|
const re2 = segmentRe(seg);
|
|
118275
118984
|
for (const name of names.sort()) {
|
|
118276
118985
|
if (name.startsWith(".") && !seg.startsWith(".")) continue;
|
|
118277
|
-
if (re2.test(name)) next.push(
|
|
118986
|
+
if (re2.test(name)) next.push(join32(base2, name));
|
|
118278
118987
|
}
|
|
118279
118988
|
}
|
|
118280
118989
|
found = next;
|
|
@@ -118329,11 +119038,11 @@ function readSshConfig(opts) {
|
|
|
118329
119038
|
}
|
|
118330
119039
|
for (const arg of split2.args) {
|
|
118331
119040
|
let path2 = arg;
|
|
118332
|
-
if (path2.startsWith("~/")) path2 =
|
|
119041
|
+
if (path2.startsWith("~/")) path2 = join32(opts.home, path2.slice(2));
|
|
118333
119042
|
else if (path2.startsWith("~")) {
|
|
118334
119043
|
notes.push(`An Include of another user's folder (${where(file2, n + 1)}) was not read`);
|
|
118335
119044
|
continue;
|
|
118336
|
-
} else if (!isAbsolute10(path2)) path2 =
|
|
119045
|
+
} else if (!isAbsolute10(path2)) path2 = join32(opts.home, ".ssh", path2);
|
|
118337
119046
|
for (const f of expandGlob(path2)) read(f, depth + 1);
|
|
118338
119047
|
}
|
|
118339
119048
|
}
|
|
@@ -118356,7 +119065,7 @@ function parseSshG(stdout) {
|
|
|
118356
119065
|
return { hostName: first.get("hostname") || null, user: first.get("user") || null, port: Number.isInteger(port2) && port2 > 0 ? port2 : null };
|
|
118357
119066
|
}
|
|
118358
119067
|
function resolveAlias(alias, opts = {}) {
|
|
118359
|
-
return new Promise((
|
|
119068
|
+
return new Promise((resolve12) => {
|
|
118360
119069
|
const args = [...opts.configFile ? ["-F", opts.configFile] : [], "-G", "--", alias];
|
|
118361
119070
|
let stdout = "";
|
|
118362
119071
|
let stderr = "";
|
|
@@ -118365,7 +119074,7 @@ function resolveAlias(alias, opts = {}) {
|
|
|
118365
119074
|
if (done) return;
|
|
118366
119075
|
done = true;
|
|
118367
119076
|
clearTimeout(timer);
|
|
118368
|
-
|
|
119077
|
+
resolve12(r);
|
|
118369
119078
|
};
|
|
118370
119079
|
const none = (error51) => ({ hostName: null, user: null, port: null, error: error51 });
|
|
118371
119080
|
const child = spawn7(opts.ssh ?? "ssh", args, { env: opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -118383,18 +119092,18 @@ var MAX_HOSTS = 100;
|
|
|
118383
119092
|
var RESOLVE_AT_ONCE = 8;
|
|
118384
119093
|
async function listSshHosts(opts = {}) {
|
|
118385
119094
|
const home = opts.home ?? homedir13();
|
|
118386
|
-
const file2 = opts.configFile ??
|
|
119095
|
+
const file2 = opts.configFile ?? join32(home, ".ssh", "config");
|
|
118387
119096
|
const read = readSshConfig({ file: file2, home });
|
|
118388
119097
|
const notes = [...read.notes];
|
|
118389
119098
|
const listed = read.hosts.slice(0, MAX_HOSTS);
|
|
118390
119099
|
if (read.hosts.length > MAX_HOSTS) notes.push(`Only the first ${MAX_HOSTS} of ${read.hosts.length} hosts are listed; type any other below`);
|
|
118391
|
-
const
|
|
119100
|
+
const resolve12 = opts.resolve ?? ((alias) => resolveAlias(alias, opts));
|
|
118392
119101
|
const resolved = new Array(listed.length);
|
|
118393
119102
|
let next = 0;
|
|
118394
119103
|
const worker = async () => {
|
|
118395
119104
|
while (next < listed.length) {
|
|
118396
119105
|
const i = next++;
|
|
118397
|
-
resolved[i] = await
|
|
119106
|
+
resolved[i] = await resolve12(listed[i].alias);
|
|
118398
119107
|
}
|
|
118399
119108
|
};
|
|
118400
119109
|
await Promise.all(Array.from({ length: Math.min(RESOLVE_AT_ONCE, listed.length) }, worker));
|
|
@@ -118883,11 +119592,11 @@ var RemoteClient = class {
|
|
|
118883
119592
|
if (!this.ready || !this.ws) return Promise.reject(linkError("The machine is not reachable right now", true));
|
|
118884
119593
|
const ws2 = this.ws;
|
|
118885
119594
|
const id2 = String(this.nextId++);
|
|
118886
|
-
return new Promise((
|
|
119595
|
+
return new Promise((resolve12, reject) => {
|
|
118887
119596
|
const timer = setTimeout(() => {
|
|
118888
119597
|
if (this.pending.delete(id2)) reject(Object.assign(new Error(`The machine did not answer ${method} in time`), { code: "internal", retryable: true }));
|
|
118889
119598
|
}, this.opts.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT_MS);
|
|
118890
|
-
this.pending.set(id2, { resolve:
|
|
119599
|
+
this.pending.set(id2, { resolve: resolve12, reject, timer });
|
|
118891
119600
|
ws2.send(JSON.stringify({ kind: "rpc", id: id2, method, params }));
|
|
118892
119601
|
});
|
|
118893
119602
|
}
|
|
@@ -119250,14 +119959,14 @@ var LinkedMachine = class {
|
|
|
119250
119959
|
if (this.client?.connected) return this.client.call("host.activity", {}).then(read, () => null);
|
|
119251
119960
|
const e = this.endpoint;
|
|
119252
119961
|
if (!e?.url || this.versions?.compatible === false || this.held) return Promise.resolve(null);
|
|
119253
|
-
return new Promise((
|
|
119962
|
+
return new Promise((resolve12) => {
|
|
119254
119963
|
let settled = false;
|
|
119255
119964
|
const done = (v) => {
|
|
119256
119965
|
if (settled) return;
|
|
119257
119966
|
settled = true;
|
|
119258
119967
|
clearTimeout(timer);
|
|
119259
119968
|
client.close();
|
|
119260
|
-
|
|
119969
|
+
resolve12(v);
|
|
119261
119970
|
};
|
|
119262
119971
|
const client = new RemoteClient(
|
|
119263
119972
|
{ ...this.deps.client, url: e.url, token: e.token, handshakeTimeoutMs: 5e3, callTimeoutMs: 5e3 },
|
|
@@ -119331,16 +120040,16 @@ var LinkedMachine = class {
|
|
|
119331
120040
|
const started = await this.tunnel.startHost();
|
|
119332
120041
|
this.hostStarted = { ...started, at: Date.now() };
|
|
119333
120042
|
},
|
|
119334
|
-
answers: (version2, ms3) => new Promise((
|
|
120043
|
+
answers: (version2, ms3) => new Promise((resolve12) => {
|
|
119335
120044
|
const waiter = (v) => {
|
|
119336
120045
|
if (v !== version2) return;
|
|
119337
120046
|
clearTimeout(timer);
|
|
119338
120047
|
this.helloWaiters.delete(waiter);
|
|
119339
|
-
|
|
120048
|
+
resolve12(true);
|
|
119340
120049
|
};
|
|
119341
120050
|
const timer = setTimeout(() => {
|
|
119342
120051
|
this.helloWaiters.delete(waiter);
|
|
119343
|
-
void hold().then(() =>
|
|
120052
|
+
void hold().then(() => resolve12(false));
|
|
119344
120053
|
}, this.deps.checkMs ?? ms3);
|
|
119345
120054
|
this.helloWaiters.add(waiter);
|
|
119346
120055
|
this.held = false;
|
|
@@ -120129,6 +120838,7 @@ var ROUTES = {
|
|
|
120129
120838
|
* link, not the window, so its views are opened without one and live until closed, as before
|
|
120130
120839
|
*/
|
|
120131
120840
|
"apps.openView": { kind: "project", orMachine: true, result: (r, q2) => q2.openedView(r), params: ({ lease: _lease, ...p }) => p },
|
|
120841
|
+
"apps.recoverView": { kind: "project", orMachine: true, result: (r, q2) => q2.openedView(r), params: ({ lease: _lease, ...p }) => p },
|
|
120132
120842
|
"apps.readResource": view(["projectId", "instanceId"]),
|
|
120133
120843
|
"apps.invoke": view(["projectId", "instanceId"]),
|
|
120134
120844
|
"apps.inlineReopen": session((r, q2) => q2.inlineView(r)),
|
|
@@ -120488,7 +121198,7 @@ function storeMirror(store2) {
|
|
|
120488
121198
|
var start = new StartSequence();
|
|
120489
121199
|
start.at("launch variables");
|
|
120490
121200
|
var launch = takeLaunchEnv();
|
|
120491
|
-
var BUILD = true ? "
|
|
121201
|
+
var BUILD = true ? "5dca7b8" : "dev";
|
|
120492
121202
|
var { values } = parseArgs({
|
|
120493
121203
|
options: {
|
|
120494
121204
|
port: { type: "string", default: "5175" },
|
|
@@ -120509,12 +121219,12 @@ start.at("data folder");
|
|
|
120509
121219
|
var movedNote = null;
|
|
120510
121220
|
function defaultDbPath() {
|
|
120511
121221
|
const isDev = process.env.CC_DEV === "1";
|
|
120512
|
-
const dir =
|
|
120513
|
-
const legacy =
|
|
121222
|
+
const dir = join33(homedir14(), isDev ? DATA_DIR_DEV : DATA_DIR);
|
|
121223
|
+
const legacy = join33(homedir14(), isDev ? DATA_DIR_LEGACY.dev : DATA_DIR_LEGACY.prod);
|
|
120514
121224
|
movedNote = migrateLegacyDataDir(legacy, dir) ? `[agent-host] data folder moved: ${legacy} -> ${dir}` : null;
|
|
120515
121225
|
mkdirSync11(dir, { recursive: true });
|
|
120516
121226
|
process.env.CC_DATA_DIR = dir;
|
|
120517
|
-
return
|
|
121227
|
+
return join33(dir, "store.db");
|
|
120518
121228
|
}
|
|
120519
121229
|
var dbPath = values.memory ? ":memory:" : values.db ?? defaultDbPath();
|
|
120520
121230
|
start.at("log");
|
|
@@ -120592,7 +121302,7 @@ var mgr = new SessionManager(
|
|
|
120592
121302
|
},
|
|
120593
121303
|
(sessionId) => bridgeAddress(launch.frontDoor, port, token, sessionId),
|
|
120594
121304
|
// Worktrees are created next to the data folder — dev and the packaged app never touch each other's worktrees
|
|
120595
|
-
|
|
121305
|
+
join33(dirname11(dbPath), "worktrees"),
|
|
120596
121306
|
held2 ? { processes: held2.processes, keptSessions: held2.kept.sessionIds } : {}
|
|
120597
121307
|
);
|
|
120598
121308
|
mgr.useProjectsHome(() => projectsHome());
|
|
@@ -120626,7 +121336,7 @@ if (held2) {
|
|
|
120626
121336
|
}
|
|
120627
121337
|
var AUTO_UPDATE_CHECK_KEY = "updates.auto";
|
|
120628
121338
|
var AUTO_APPLY_UPDATES_KEY = "updates.autoApply";
|
|
120629
|
-
var themes = new ThemeFiles(
|
|
121339
|
+
var themes = new ThemeFiles(join33(dataRoot(), "themes"), () => events.push({ type: "themes_changed" }));
|
|
120630
121340
|
await themes.start().catch((e) => console.error(`[themes] the themes folder is unavailable: ${e.message}`));
|
|
120631
121341
|
var updates = new UpdateService((status) => events.push({ type: "update_status", status }), {
|
|
120632
121342
|
// On by default when nothing has been saved yet. Since the check is read-only and every failure
|
|
@@ -120845,7 +121555,7 @@ var endings = wireEndings({
|
|
|
120845
121555
|
heldChildren: held2 !== null,
|
|
120846
121556
|
leave: (mode, handOver) => void shutdown.leave(mode, handOver),
|
|
120847
121557
|
force: (why) => shutdown.force(why),
|
|
120848
|
-
record: crashRecorder(
|
|
121558
|
+
record: crashRecorder(join33(dirname11(dbPath), "host-errors.log"), (line) => console.error(line)),
|
|
120849
121559
|
stdin: process.stdin,
|
|
120850
121560
|
watchParent: values["watch-parent"] === true,
|
|
120851
121561
|
log: (line) => console.error(line)
|