@harness-engineering/orchestrator 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +284 -58
- package/dist/index.mjs +261 -33
- package/package.json +8 -8
package/dist/index.d.mts
CHANGED
|
@@ -1993,6 +1993,15 @@ declare class Orchestrator extends EventEmitter {
|
|
|
1993
1993
|
* `store.load()`.
|
|
1994
1994
|
*/
|
|
1995
1995
|
private initModelPool;
|
|
1996
|
+
/**
|
|
1997
|
+
* LMLM Phase 7 wiring: apply the operator's configured pool bounds (disk
|
|
1998
|
+
* budget + org/family allowlist) from `localModels.pool` to the live pool.
|
|
1999
|
+
* Called after `PoolStateStore.load()` so config wins over persisted bounds
|
|
2000
|
+
* (D2, declarative precedence). No-op when the pool is null (LMLM disabled)
|
|
2001
|
+
* or no `pool` block is configured, so it is safe to call unconditionally
|
|
2002
|
+
* on the startup path.
|
|
2003
|
+
*/
|
|
2004
|
+
private applyConfiguredPoolBounds;
|
|
1996
2005
|
/**
|
|
1997
2006
|
* LMLM Phase 6: arm the single background refresh scheduler over the live
|
|
1998
2007
|
* pool. No-op when LMLM is disabled (`modelPool` null). Each tick runs
|
package/dist/index.d.ts
CHANGED
|
@@ -1993,6 +1993,15 @@ declare class Orchestrator extends EventEmitter {
|
|
|
1993
1993
|
* `store.load()`.
|
|
1994
1994
|
*/
|
|
1995
1995
|
private initModelPool;
|
|
1996
|
+
/**
|
|
1997
|
+
* LMLM Phase 7 wiring: apply the operator's configured pool bounds (disk
|
|
1998
|
+
* budget + org/family allowlist) from `localModels.pool` to the live pool.
|
|
1999
|
+
* Called after `PoolStateStore.load()` so config wins over persisted bounds
|
|
2000
|
+
* (D2, declarative precedence). No-op when the pool is null (LMLM disabled)
|
|
2001
|
+
* or no `pool` block is configured, so it is safe to call unconditionally
|
|
2002
|
+
* on the startup path.
|
|
2003
|
+
*/
|
|
2004
|
+
private applyConfiguredPoolBounds;
|
|
1996
2005
|
/**
|
|
1997
2006
|
* LMLM Phase 6: arm the single background refresh scheduler over the live
|
|
1998
2007
|
* pool. No-op when LMLM is disabled (`modelPool` null). Each tick runs
|
package/dist/index.js
CHANGED
|
@@ -3109,7 +3109,7 @@ var PromptRenderer = class {
|
|
|
3109
3109
|
var import_node_events = require("events");
|
|
3110
3110
|
var path21 = __toESM(require("path"));
|
|
3111
3111
|
var import_node_crypto15 = require("crypto");
|
|
3112
|
-
var
|
|
3112
|
+
var import_core16 = require("@harness-engineering/core");
|
|
3113
3113
|
|
|
3114
3114
|
// src/core/stall-detector.ts
|
|
3115
3115
|
function detectStalledIssues(running, nowMs, stallTimeoutMs) {
|
|
@@ -3685,7 +3685,7 @@ var CompletionHandler = class {
|
|
|
3685
3685
|
};
|
|
3686
3686
|
|
|
3687
3687
|
// src/orchestrator.ts
|
|
3688
|
-
var
|
|
3688
|
+
var import_core17 = require("@harness-engineering/core");
|
|
3689
3689
|
|
|
3690
3690
|
// src/tracker/adapters/github-issues-issue-tracker.ts
|
|
3691
3691
|
var import_types9 = require("@harness-engineering/types");
|
|
@@ -4085,7 +4085,7 @@ var LocalModelResolver = class {
|
|
|
4085
4085
|
|
|
4086
4086
|
// src/orchestrator.ts
|
|
4087
4087
|
var import_local_models4 = require("@harness-engineering/local-models");
|
|
4088
|
-
var
|
|
4088
|
+
var import_core18 = require("@harness-engineering/core");
|
|
4089
4089
|
|
|
4090
4090
|
// src/agent/config-migration.ts
|
|
4091
4091
|
var MIGRATION_GUIDE = "docs/guides/multi-backend-routing.md";
|
|
@@ -7049,7 +7049,7 @@ function buildRoutingUseCase(issue, backendParam, catalog) {
|
|
|
7049
7049
|
// src/server/http.ts
|
|
7050
7050
|
var http = __toESM(require("http"));
|
|
7051
7051
|
var path17 = __toESM(require("path"));
|
|
7052
|
-
var
|
|
7052
|
+
var import_core12 = require("@harness-engineering/core");
|
|
7053
7053
|
|
|
7054
7054
|
// src/server/websocket.ts
|
|
7055
7055
|
var import_ws = require("ws");
|
|
@@ -9186,17 +9186,187 @@ async function runForceRefresh(res, scheduler, deps) {
|
|
|
9186
9186
|
});
|
|
9187
9187
|
}
|
|
9188
9188
|
|
|
9189
|
+
// src/server/routes/v1/local-models-pool-mutation.ts
|
|
9190
|
+
var import_core11 = require("@harness-engineering/core");
|
|
9191
|
+
var INSTALL_RE = /^\/api\/v1\/local-models\/pool\/install(?:\?.*)?$/;
|
|
9192
|
+
var REMOVE_RE = /^\/api\/v1\/local-models\/pool\/remove(?:\?.*)?$/;
|
|
9193
|
+
var RESOLVE_TOP = 50;
|
|
9194
|
+
function sendJSON10(res, status, body) {
|
|
9195
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
9196
|
+
res.end(JSON.stringify(body));
|
|
9197
|
+
}
|
|
9198
|
+
function decidedByOf(deps, req) {
|
|
9199
|
+
if (deps.getDecidedBy) return deps.getDecidedBy(req);
|
|
9200
|
+
const token = req._authToken;
|
|
9201
|
+
return token?.id ?? "operator";
|
|
9202
|
+
}
|
|
9203
|
+
function handlerDeps(deps, req, pool) {
|
|
9204
|
+
return {
|
|
9205
|
+
pool,
|
|
9206
|
+
bus: deps.bus,
|
|
9207
|
+
updateProposal: (id, patch) => (0, import_core11.updateProposal)(deps.projectPath, id, patch),
|
|
9208
|
+
decidedBy: decidedByOf(deps, req),
|
|
9209
|
+
...deps.isModelInUse ? { isModelInUse: deps.isModelInUse } : {}
|
|
9210
|
+
};
|
|
9211
|
+
}
|
|
9212
|
+
async function readJsonBody(req) {
|
|
9213
|
+
try {
|
|
9214
|
+
const raw = await readBody(req);
|
|
9215
|
+
if (raw.length === 0) return {};
|
|
9216
|
+
const parsed = JSON.parse(raw);
|
|
9217
|
+
return typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
9218
|
+
} catch {
|
|
9219
|
+
return void 0;
|
|
9220
|
+
}
|
|
9221
|
+
}
|
|
9222
|
+
async function handleInstall(req, res, deps) {
|
|
9223
|
+
const pool = deps.getModelPool();
|
|
9224
|
+
if (pool === null) return sendJSON10(res, 503, { error: "LMLM disabled" });
|
|
9225
|
+
const body = await readJsonBody(req);
|
|
9226
|
+
if (body === void 0) return sendJSON10(res, 400, { error: "invalid JSON body" });
|
|
9227
|
+
const hfRepoId = typeof body.hfRepoId === "string" ? body.hfRepoId : "";
|
|
9228
|
+
if (hfRepoId.length === 0) return sendJSON10(res, 400, { error: "hfRepoId is required" });
|
|
9229
|
+
const quant = typeof body.quant === "string" ? body.quant : void 0;
|
|
9230
|
+
if (!deps.getRecommendations) {
|
|
9231
|
+
return sendJSON10(res, 503, { error: "recommendations unavailable" });
|
|
9232
|
+
}
|
|
9233
|
+
const ranked = await deps.getRecommendations({ top: RESOLVE_TOP, profile: "general" });
|
|
9234
|
+
const match = ranked.find(
|
|
9235
|
+
(r) => r.hfRepoId === hfRepoId && (quant === void 0 || r.quant === quant)
|
|
9236
|
+
);
|
|
9237
|
+
if (!match) {
|
|
9238
|
+
return sendJSON10(res, 404, {
|
|
9239
|
+
error: `no recommendation for ${hfRepoId}${quant ? ` @ ${quant}` : ""}`
|
|
9240
|
+
});
|
|
9241
|
+
}
|
|
9242
|
+
if (!match.ollamaName) {
|
|
9243
|
+
return sendJSON10(res, 422, { error: `${hfRepoId} has no known Ollama mirror to install` });
|
|
9244
|
+
}
|
|
9245
|
+
const content = {
|
|
9246
|
+
action: "add",
|
|
9247
|
+
target: { hfRepoId: match.hfRepoId, ollamaName: match.ollamaName },
|
|
9248
|
+
scoreDelta: match.score,
|
|
9249
|
+
justification: {
|
|
9250
|
+
summary: `Operator-initiated install of ${match.ollamaName} (${match.quant}).`,
|
|
9251
|
+
benchmarkBasis: [],
|
|
9252
|
+
hardwareFit: match.fitsHardware ? "fits detected hardware" : "may not fit detected hardware",
|
|
9253
|
+
evidence: match.evidence,
|
|
9254
|
+
freshness: `snapshot ${match.benchmarkSnapshot}`
|
|
9255
|
+
},
|
|
9256
|
+
// 0 → the installer resolves the true on-disk size via `inspect` before the
|
|
9257
|
+
// budget check, rather than trusting an estimate.
|
|
9258
|
+
diskImpactGb: 0
|
|
9259
|
+
};
|
|
9260
|
+
const record = await (0, import_core11.createModelProposal)(deps.projectPath, content, {
|
|
9261
|
+
proposedBy: decidedByOf(deps, req)
|
|
9262
|
+
});
|
|
9263
|
+
const outcome = await onApproveModelProposal(handlerDeps(deps, req, pool), record);
|
|
9264
|
+
if (outcome.status === "approved") {
|
|
9265
|
+
const result = {
|
|
9266
|
+
disposition: "installed",
|
|
9267
|
+
proposalId: record.id,
|
|
9268
|
+
evicted: outcome.evicted.map((e) => e.ollamaName)
|
|
9269
|
+
};
|
|
9270
|
+
return sendJSON10(res, 200, result);
|
|
9271
|
+
}
|
|
9272
|
+
if (outcome.status === "failed_target_missing") {
|
|
9273
|
+
return sendJSON10(res, 404, {
|
|
9274
|
+
error: `${hfRepoId} is no longer available on HuggingFace`,
|
|
9275
|
+
proposalId: record.id
|
|
9276
|
+
});
|
|
9277
|
+
}
|
|
9278
|
+
const status = outcome.code === "not_allowed" || outcome.code === "budget_exceeded" ? 409 : 502;
|
|
9279
|
+
return sendJSON10(res, status, {
|
|
9280
|
+
error: outcome.message,
|
|
9281
|
+
code: outcome.code,
|
|
9282
|
+
proposalId: record.id
|
|
9283
|
+
});
|
|
9284
|
+
}
|
|
9285
|
+
async function handleRemove(req, res, deps) {
|
|
9286
|
+
const pool = deps.getModelPool();
|
|
9287
|
+
if (pool === null) return sendJSON10(res, 503, { error: "LMLM disabled" });
|
|
9288
|
+
const body = await readJsonBody(req);
|
|
9289
|
+
if (body === void 0) return sendJSON10(res, 400, { error: "invalid JSON body" });
|
|
9290
|
+
const ollamaName = typeof body.ollamaName === "string" ? body.ollamaName : "";
|
|
9291
|
+
if (ollamaName.length === 0) return sendJSON10(res, 400, { error: "ollamaName is required" });
|
|
9292
|
+
const entry = pool.snapshot().entries.find((e) => e.ollamaName === ollamaName);
|
|
9293
|
+
if (!entry) return sendJSON10(res, 404, { error: `${ollamaName} is not in the pool` });
|
|
9294
|
+
const content = {
|
|
9295
|
+
action: "evict",
|
|
9296
|
+
target: { hfRepoId: entry.hfRepoId, ollamaName },
|
|
9297
|
+
scoreDelta: 0,
|
|
9298
|
+
justification: {
|
|
9299
|
+
summary: `Operator-initiated removal of ${ollamaName}.`,
|
|
9300
|
+
benchmarkBasis: [],
|
|
9301
|
+
hardwareFit: "",
|
|
9302
|
+
evidence: "operator",
|
|
9303
|
+
freshness: ""
|
|
9304
|
+
},
|
|
9305
|
+
diskImpactGb: entry.sizeOnDiskGb
|
|
9306
|
+
};
|
|
9307
|
+
const record = await (0, import_core11.createModelProposal)(deps.projectPath, content, {
|
|
9308
|
+
proposedBy: decidedByOf(deps, req)
|
|
9309
|
+
});
|
|
9310
|
+
const outcome = await onApproveModelProposal(handlerDeps(deps, req, pool), record);
|
|
9311
|
+
if (outcome.status === "error") {
|
|
9312
|
+
return sendJSON10(res, 502, {
|
|
9313
|
+
error: outcome.message,
|
|
9314
|
+
code: outcome.code,
|
|
9315
|
+
proposalId: record.id
|
|
9316
|
+
});
|
|
9317
|
+
}
|
|
9318
|
+
if (outcome.status === "failed_target_missing") {
|
|
9319
|
+
return sendJSON10(res, 500, {
|
|
9320
|
+
error: "unexpected failed_target_missing on evict",
|
|
9321
|
+
proposalId: record.id
|
|
9322
|
+
});
|
|
9323
|
+
}
|
|
9324
|
+
if (outcome.evicted.length > 0) {
|
|
9325
|
+
const result2 = {
|
|
9326
|
+
disposition: "removed",
|
|
9327
|
+
proposalId: record.id,
|
|
9328
|
+
evicted: outcome.evicted.map((e) => e.ollamaName)
|
|
9329
|
+
};
|
|
9330
|
+
return sendJSON10(res, 200, result2);
|
|
9331
|
+
}
|
|
9332
|
+
const stillPresent = pool.snapshot().entries.some((e) => e.ollamaName === ollamaName);
|
|
9333
|
+
if (stillPresent) {
|
|
9334
|
+
const result2 = {
|
|
9335
|
+
disposition: "deferred",
|
|
9336
|
+
proposalId: record.id,
|
|
9337
|
+
evicted: [],
|
|
9338
|
+
message: "model is in use; it will be removed after the current run"
|
|
9339
|
+
};
|
|
9340
|
+
return sendJSON10(res, 202, result2);
|
|
9341
|
+
}
|
|
9342
|
+
const result = { disposition: "removed", proposalId: record.id, evicted: [] };
|
|
9343
|
+
return sendJSON10(res, 200, result);
|
|
9344
|
+
}
|
|
9345
|
+
function handleV1LocalModelsMutationRoute(req, res, deps) {
|
|
9346
|
+
const url = req.url ?? "";
|
|
9347
|
+
if ((req.method ?? "GET") !== "POST") return false;
|
|
9348
|
+
if (INSTALL_RE.test(url)) {
|
|
9349
|
+
void handleInstall(req, res, deps);
|
|
9350
|
+
return true;
|
|
9351
|
+
}
|
|
9352
|
+
if (REMOVE_RE.test(url)) {
|
|
9353
|
+
void handleRemove(req, res, deps);
|
|
9354
|
+
return true;
|
|
9355
|
+
}
|
|
9356
|
+
return false;
|
|
9357
|
+
}
|
|
9358
|
+
|
|
9189
9359
|
// src/server/routes/v1/routing.ts
|
|
9190
9360
|
var import_zod14 = require("zod");
|
|
9191
9361
|
var CONFIG_RE = /^\/api\/v1\/routing\/config(?:\?.*)?$/;
|
|
9192
9362
|
var DECISIONS_RE = /^\/api\/v1\/routing\/decisions(?:\?.*)?$/;
|
|
9193
9363
|
var TRACE_RE = /^\/api\/v1\/routing\/trace(?:\?.*)?$/;
|
|
9194
|
-
function
|
|
9364
|
+
function sendJSON11(res, status, body) {
|
|
9195
9365
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
9196
9366
|
res.end(JSON.stringify(body));
|
|
9197
9367
|
}
|
|
9198
9368
|
function unavailable(res) {
|
|
9199
|
-
|
|
9369
|
+
sendJSON11(res, 503, { error: "BackendRouter not available" });
|
|
9200
9370
|
return true;
|
|
9201
9371
|
}
|
|
9202
9372
|
function resolveChain(value, backends) {
|
|
@@ -9233,7 +9403,7 @@ function buildResolvedChains(routing, backends) {
|
|
|
9233
9403
|
}
|
|
9234
9404
|
function handleConfig(res, deps) {
|
|
9235
9405
|
if (!deps.router || !deps.routing || !deps.backends) return unavailable(res);
|
|
9236
|
-
|
|
9406
|
+
sendJSON11(res, 200, {
|
|
9237
9407
|
routing: deps.routing,
|
|
9238
9408
|
resolvedChains: buildResolvedChains(deps.routing, deps.backends),
|
|
9239
9409
|
backends: Object.keys(deps.backends)
|
|
@@ -9261,7 +9431,7 @@ function parseDecisionsQuery(url) {
|
|
|
9261
9431
|
function handleDecisions(req, res, deps) {
|
|
9262
9432
|
if (!deps.bus) return unavailable(res);
|
|
9263
9433
|
const filter = parseDecisionsQuery(req.url ?? "");
|
|
9264
|
-
|
|
9434
|
+
sendJSON11(res, 200, { decisions: deps.bus.recent(filter) });
|
|
9265
9435
|
return true;
|
|
9266
9436
|
}
|
|
9267
9437
|
var UseCaseSchema = import_zod14.z.discriminatedUnion("kind", [
|
|
@@ -9293,19 +9463,19 @@ async function handleTrace(req, res, deps) {
|
|
|
9293
9463
|
try {
|
|
9294
9464
|
raw = await readBody(req);
|
|
9295
9465
|
} catch {
|
|
9296
|
-
|
|
9466
|
+
sendJSON11(res, 400, { error: "body read failed" });
|
|
9297
9467
|
return true;
|
|
9298
9468
|
}
|
|
9299
9469
|
let parsed;
|
|
9300
9470
|
try {
|
|
9301
9471
|
parsed = JSON.parse(raw);
|
|
9302
9472
|
} catch {
|
|
9303
|
-
|
|
9473
|
+
sendJSON11(res, 400, { error: "invalid JSON body" });
|
|
9304
9474
|
return true;
|
|
9305
9475
|
}
|
|
9306
9476
|
const r = TraceBodySchema.safeParse(parsed);
|
|
9307
9477
|
if (!r.success) {
|
|
9308
|
-
|
|
9478
|
+
sendJSON11(res, 400, { error: r.error.message });
|
|
9309
9479
|
return true;
|
|
9310
9480
|
}
|
|
9311
9481
|
const opts = r.data.invocationOverride !== void 0 ? { invocationOverride: r.data.invocationOverride } : void 0;
|
|
@@ -9318,9 +9488,9 @@ async function handleTrace(req, res, deps) {
|
|
|
9318
9488
|
r.data.useCase,
|
|
9319
9489
|
opts
|
|
9320
9490
|
);
|
|
9321
|
-
|
|
9491
|
+
sendJSON11(res, 200, { decision, def: { type: def.type } });
|
|
9322
9492
|
} catch (err) {
|
|
9323
|
-
|
|
9493
|
+
sendJSON11(res, 500, { error: String(err) });
|
|
9324
9494
|
}
|
|
9325
9495
|
return true;
|
|
9326
9496
|
}
|
|
@@ -9557,7 +9727,7 @@ var CreateBodySchema = import_zod16.z.object({
|
|
|
9557
9727
|
tenantId: import_zod16.z.string().optional(),
|
|
9558
9728
|
expiresAt: import_zod16.z.string().datetime().optional()
|
|
9559
9729
|
});
|
|
9560
|
-
function
|
|
9730
|
+
function sendJSON12(res, status, body) {
|
|
9561
9731
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
9562
9732
|
res.end(JSON.stringify(body));
|
|
9563
9733
|
}
|
|
@@ -9567,19 +9737,19 @@ async function handlePost(req, res, store) {
|
|
|
9567
9737
|
raw = await readBody(req);
|
|
9568
9738
|
} catch (err) {
|
|
9569
9739
|
const msg = err instanceof Error ? err.message : "Failed to read body";
|
|
9570
|
-
|
|
9740
|
+
sendJSON12(res, 413, { error: msg });
|
|
9571
9741
|
return;
|
|
9572
9742
|
}
|
|
9573
9743
|
let json;
|
|
9574
9744
|
try {
|
|
9575
9745
|
json = JSON.parse(raw);
|
|
9576
9746
|
} catch {
|
|
9577
|
-
|
|
9747
|
+
sendJSON12(res, 400, { error: "Invalid JSON body" });
|
|
9578
9748
|
return;
|
|
9579
9749
|
}
|
|
9580
9750
|
const parsed = CreateBodySchema.safeParse(json);
|
|
9581
9751
|
if (!parsed.success) {
|
|
9582
|
-
|
|
9752
|
+
sendJSON12(res, 422, { error: "Invalid body", issues: parsed.error.issues });
|
|
9583
9753
|
return;
|
|
9584
9754
|
}
|
|
9585
9755
|
try {
|
|
@@ -9592,37 +9762,37 @@ async function handlePost(req, res, store) {
|
|
|
9592
9762
|
if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
|
|
9593
9763
|
const result = await store.create(input);
|
|
9594
9764
|
const publicRecord = import_types25.AuthTokenPublicSchema.parse(result.record);
|
|
9595
|
-
|
|
9765
|
+
sendJSON12(res, 200, {
|
|
9596
9766
|
...publicRecord,
|
|
9597
9767
|
token: result.token
|
|
9598
9768
|
});
|
|
9599
9769
|
} catch (err) {
|
|
9600
9770
|
const msg = err instanceof Error ? err.message : "Failed to create token";
|
|
9601
9771
|
if (msg.includes("already exists")) {
|
|
9602
|
-
|
|
9772
|
+
sendJSON12(res, 409, { error: msg });
|
|
9603
9773
|
return;
|
|
9604
9774
|
}
|
|
9605
|
-
|
|
9775
|
+
sendJSON12(res, 500, { error: "Internal error creating token" });
|
|
9606
9776
|
}
|
|
9607
9777
|
}
|
|
9608
9778
|
async function handleList3(res, store) {
|
|
9609
9779
|
try {
|
|
9610
9780
|
const list = await store.list();
|
|
9611
|
-
|
|
9781
|
+
sendJSON12(res, 200, list);
|
|
9612
9782
|
} catch {
|
|
9613
|
-
|
|
9783
|
+
sendJSON12(res, 500, { error: "Internal error listing tokens" });
|
|
9614
9784
|
}
|
|
9615
9785
|
}
|
|
9616
9786
|
async function handleDelete2(res, store, id) {
|
|
9617
9787
|
try {
|
|
9618
9788
|
const ok = await store.revoke(id);
|
|
9619
9789
|
if (!ok) {
|
|
9620
|
-
|
|
9790
|
+
sendJSON12(res, 404, { error: "Token not found" });
|
|
9621
9791
|
return;
|
|
9622
9792
|
}
|
|
9623
|
-
|
|
9793
|
+
sendJSON12(res, 200, { deleted: true });
|
|
9624
9794
|
} catch {
|
|
9625
|
-
|
|
9795
|
+
sendJSON12(res, 500, { error: "Internal error revoking token" });
|
|
9626
9796
|
}
|
|
9627
9797
|
}
|
|
9628
9798
|
var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
|
|
@@ -9647,12 +9817,12 @@ function handleAuthRoute(req, res, store) {
|
|
|
9647
9817
|
return true;
|
|
9648
9818
|
}
|
|
9649
9819
|
}
|
|
9650
|
-
|
|
9820
|
+
sendJSON12(res, 405, { error: "Method not allowed" });
|
|
9651
9821
|
return true;
|
|
9652
9822
|
}
|
|
9653
9823
|
|
|
9654
9824
|
// src/server/routes/local-model.ts
|
|
9655
|
-
function
|
|
9825
|
+
function sendJSON13(res, status, body) {
|
|
9656
9826
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
9657
9827
|
res.end(JSON.stringify(body));
|
|
9658
9828
|
}
|
|
@@ -9660,30 +9830,30 @@ function handleLocalModelRoute(req, res, getStatus) {
|
|
|
9660
9830
|
const { method, url } = req;
|
|
9661
9831
|
if (url !== "/api/v1/local-model/status") return false;
|
|
9662
9832
|
if (method !== "GET") {
|
|
9663
|
-
|
|
9833
|
+
sendJSON13(res, 405, { error: "Method not allowed" });
|
|
9664
9834
|
return true;
|
|
9665
9835
|
}
|
|
9666
9836
|
if (!getStatus) {
|
|
9667
|
-
|
|
9837
|
+
sendJSON13(res, 503, { error: "Local backend not configured" });
|
|
9668
9838
|
return true;
|
|
9669
9839
|
}
|
|
9670
9840
|
const status = getStatus();
|
|
9671
9841
|
if (!status) {
|
|
9672
|
-
|
|
9842
|
+
sendJSON13(res, 503, { error: "Local backend not configured" });
|
|
9673
9843
|
return true;
|
|
9674
9844
|
}
|
|
9675
|
-
|
|
9845
|
+
sendJSON13(res, 200, status);
|
|
9676
9846
|
return true;
|
|
9677
9847
|
}
|
|
9678
9848
|
function handleLocalModelsRoute(req, res, getStatuses) {
|
|
9679
9849
|
const { method, url } = req;
|
|
9680
9850
|
if (url !== "/api/v1/local-models/status") return false;
|
|
9681
9851
|
if (method !== "GET") {
|
|
9682
|
-
|
|
9852
|
+
sendJSON13(res, 405, { error: "Method not allowed" });
|
|
9683
9853
|
return true;
|
|
9684
9854
|
}
|
|
9685
9855
|
const statuses = getStatuses ? getStatuses() : [];
|
|
9686
|
-
|
|
9856
|
+
sendJSON13(res, 200, statuses);
|
|
9687
9857
|
return true;
|
|
9688
9858
|
}
|
|
9689
9859
|
|
|
@@ -10056,6 +10226,21 @@ var V1_BRIDGE_ROUTES = [
|
|
|
10056
10226
|
scope: "manage-proposals",
|
|
10057
10227
|
description: "Force a background-scheduler refresh tick and return emitted proposals (O4)."
|
|
10058
10228
|
},
|
|
10229
|
+
// ── LMLM dashboard pool mutation — operator-initiated install/remove ──
|
|
10230
|
+
// Modeled as auto-approved model proposals, so the same `manage-proposals`
|
|
10231
|
+
// write scope that governs approve/reject governs these too.
|
|
10232
|
+
{
|
|
10233
|
+
method: "POST",
|
|
10234
|
+
pattern: /^\/api\/v1\/local-models\/pool\/install(?:\?.*)?$/,
|
|
10235
|
+
scope: "manage-proposals",
|
|
10236
|
+
description: "Install a recommended model into the local pool (auto-approved proposal)."
|
|
10237
|
+
},
|
|
10238
|
+
{
|
|
10239
|
+
method: "POST",
|
|
10240
|
+
pattern: /^\/api\/v1\/local-models\/pool\/remove(?:\?.*)?$/,
|
|
10241
|
+
scope: "manage-proposals",
|
|
10242
|
+
description: "Remove a model from the local pool (auto-approved proposal)."
|
|
10243
|
+
},
|
|
10059
10244
|
// ── LMLM Phase 7 — read surface (hardware/pool/recommendations/proposals) ──
|
|
10060
10245
|
// Load-bearing: `local-models` is in `V1_WRAPPABLE`, so without these entries
|
|
10061
10246
|
// the `/api/v1` rewrite shim would rewrite `/api/v1/local-models/<name>` →
|
|
@@ -10490,6 +10675,17 @@ var OrchestratorServer = class {
|
|
|
10490
10675
|
...this.isModelInUseFn ? { isModelInUse: this.isModelInUseFn } : {}
|
|
10491
10676
|
});
|
|
10492
10677
|
},
|
|
10678
|
+
// LMLM dashboard pool mutation — POST /pool/install + /pool/remove.
|
|
10679
|
+
// Operator-initiated install/remove via auto-approved proposals; reuses
|
|
10680
|
+
// the same mutation pool + bus + in-use probe the proposals route uses.
|
|
10681
|
+
// Registered before the read surface so /pool/install|remove match first.
|
|
10682
|
+
(req, res) => handleV1LocalModelsMutationRoute(req, res, {
|
|
10683
|
+
projectPath: this.projectPath,
|
|
10684
|
+
bus: this.orchestrator,
|
|
10685
|
+
getModelPool: () => this.getModelPoolFn?.() ?? null,
|
|
10686
|
+
...this.getRecommendationsFn ? { getRecommendations: this.getRecommendationsFn } : {},
|
|
10687
|
+
...this.isModelInUseFn ? { isModelInUse: this.isModelInUseFn } : {}
|
|
10688
|
+
}),
|
|
10493
10689
|
// LMLM Phase 6/7 — POST /refresh + the GET read surface
|
|
10494
10690
|
// (hardware/pool/recommendations/proposals). Registered before the
|
|
10495
10691
|
// chat-proxy fallback so it owns the path. Each accessor is spread in
|
|
@@ -10583,7 +10779,7 @@ var OrchestratorServer = class {
|
|
|
10583
10779
|
return this.broadcaster.clientCount;
|
|
10584
10780
|
}
|
|
10585
10781
|
async start() {
|
|
10586
|
-
(0,
|
|
10782
|
+
(0, import_core12.assertPortUsable)(this.port, "orchestrator");
|
|
10587
10783
|
if (this.interactionQueue) {
|
|
10588
10784
|
this.planWatcher = new PlanWatcher(this.plansDir, this.interactionQueue);
|
|
10589
10785
|
this.planWatcher.start();
|
|
@@ -11085,7 +11281,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
|
|
|
11085
11281
|
|
|
11086
11282
|
// src/gateway/telemetry/fanout.ts
|
|
11087
11283
|
var import_node_crypto13 = require("crypto");
|
|
11088
|
-
var
|
|
11284
|
+
var import_core13 = require("@harness-engineering/core");
|
|
11089
11285
|
var TOPICS = {
|
|
11090
11286
|
MAINTENANCE_STARTED: "maintenance:started",
|
|
11091
11287
|
MAINTENANCE_COMPLETED: "maintenance:completed",
|
|
@@ -11217,7 +11413,7 @@ function buildSpan(topic, plan, payload) {
|
|
|
11217
11413
|
spanId: plan.spanId,
|
|
11218
11414
|
...plan.parentSpanId !== void 0 ? { parentSpanId: plan.parentSpanId } : {},
|
|
11219
11415
|
name: SPAN_NAME[topic],
|
|
11220
|
-
kind:
|
|
11416
|
+
kind: import_core13.SpanKind.INTERNAL,
|
|
11221
11417
|
startTimeNs: startNs,
|
|
11222
11418
|
endTimeNs: startNs,
|
|
11223
11419
|
attributes: buildAttributes(payload, { "harness.topic": topic }),
|
|
@@ -11674,7 +11870,7 @@ function wireNotificationSinks({ bus, registry }) {
|
|
|
11674
11870
|
}
|
|
11675
11871
|
|
|
11676
11872
|
// src/orchestrator.ts
|
|
11677
|
-
var
|
|
11873
|
+
var import_core19 = require("@harness-engineering/core");
|
|
11678
11874
|
|
|
11679
11875
|
// src/logging/logger.ts
|
|
11680
11876
|
var StructuredLogger = class {
|
|
@@ -11716,7 +11912,7 @@ var StructuredLogger = class {
|
|
|
11716
11912
|
// src/workspace/config-scanner.ts
|
|
11717
11913
|
var import_node_fs = require("fs");
|
|
11718
11914
|
var import_node_path4 = require("path");
|
|
11719
|
-
var
|
|
11915
|
+
var import_core14 = require("@harness-engineering/core");
|
|
11720
11916
|
var CONFIG_FILES = ["CLAUDE.md", "AGENTS.md", ".gemini/settings.json", "skill.yaml"];
|
|
11721
11917
|
var BLOCKING_INJECTION_PREFIXES = ["INJ-UNI-", "INJ-REROL-"];
|
|
11722
11918
|
var DOWNGRADED_SECURITY_RULES = /* @__PURE__ */ new Set(["SEC-AGT-006"]);
|
|
@@ -11738,29 +11934,29 @@ async function scanSingleFile(filePath, targetDir, scanner) {
|
|
|
11738
11934
|
} catch {
|
|
11739
11935
|
return null;
|
|
11740
11936
|
}
|
|
11741
|
-
const injectionFindings = (0,
|
|
11742
|
-
const findings = (0,
|
|
11937
|
+
const injectionFindings = (0, import_core14.scanForInjection)(content);
|
|
11938
|
+
const findings = (0, import_core14.mapInjectionFindings)(injectionFindings);
|
|
11743
11939
|
const secFindings = await scanner.scanFile(filePath);
|
|
11744
|
-
findings.push(...(0,
|
|
11940
|
+
findings.push(...(0, import_core14.mapSecurityFindings)(secFindings, findings));
|
|
11745
11941
|
const adjusted = adjustFindingSeverity(findings);
|
|
11746
11942
|
return {
|
|
11747
11943
|
file: (0, import_node_path4.relative)(targetDir, filePath).replaceAll("\\", "/"),
|
|
11748
11944
|
findings: adjusted,
|
|
11749
|
-
overallSeverity: (0,
|
|
11945
|
+
overallSeverity: (0, import_core14.computeOverallSeverity)(adjusted)
|
|
11750
11946
|
};
|
|
11751
11947
|
}
|
|
11752
11948
|
async function scanWorkspaceConfig(workspacePath) {
|
|
11753
|
-
const scanner = new
|
|
11949
|
+
const scanner = new import_core14.SecurityScanner((0, import_core14.parseSecurityConfig)({}));
|
|
11754
11950
|
const results = [];
|
|
11755
11951
|
for (const configFile of CONFIG_FILES) {
|
|
11756
11952
|
const result = await scanSingleFile((0, import_node_path4.join)(workspacePath, configFile), workspacePath, scanner);
|
|
11757
11953
|
if (result) results.push(result);
|
|
11758
11954
|
}
|
|
11759
|
-
return { exitCode: (0,
|
|
11955
|
+
return { exitCode: (0, import_core14.computeScanExitCode)(results), results };
|
|
11760
11956
|
}
|
|
11761
11957
|
|
|
11762
11958
|
// src/core/lane-persistence.ts
|
|
11763
|
-
var
|
|
11959
|
+
var import_core15 = require("@harness-engineering/core");
|
|
11764
11960
|
var import_types29 = require("@harness-engineering/types");
|
|
11765
11961
|
var SIGNAL_TO_LANE = {
|
|
11766
11962
|
claim: "claimed",
|
|
@@ -11774,16 +11970,16 @@ function mapOrchestratorLane(signal) {
|
|
|
11774
11970
|
}
|
|
11775
11971
|
async function persistLane(projectPath, issueId, signal) {
|
|
11776
11972
|
try {
|
|
11777
|
-
const reg = await
|
|
11973
|
+
const reg = await import_core15.eventSourcing.registerTask(projectPath, issueId, []);
|
|
11778
11974
|
if (!reg.ok) return reg;
|
|
11779
|
-
return await
|
|
11975
|
+
return await import_core15.eventSourcing.transitionLane(projectPath, issueId, mapOrchestratorLane(signal));
|
|
11780
11976
|
} catch (err) {
|
|
11781
11977
|
return (0, import_types29.Err)(err instanceof Error ? err : new Error(String(err)));
|
|
11782
11978
|
}
|
|
11783
11979
|
}
|
|
11784
11980
|
async function readPersistedLanes(projectPath) {
|
|
11785
11981
|
try {
|
|
11786
|
-
const snap = await
|
|
11982
|
+
const snap = await import_core15.eventSourcing.readSnapshot(projectPath);
|
|
11787
11983
|
return snap.ok ? snap.value.lanes : { tasks: {} };
|
|
11788
11984
|
} catch {
|
|
11789
11985
|
return { tasks: {} };
|
|
@@ -13904,7 +14100,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
13904
14100
|
this.localResolvers.set(name, new LocalModelResolver(resolverOpts));
|
|
13905
14101
|
}
|
|
13906
14102
|
}
|
|
13907
|
-
this.cacheMetrics = new
|
|
14103
|
+
this.cacheMetrics = new import_core19.CacheMetricsRecorder();
|
|
13908
14104
|
if (this.config.agent.backends !== void 0 && Object.keys(this.config.agent.backends).length > 0) {
|
|
13909
14105
|
const sandboxPolicy = this.config.agent.sandboxPolicy === "docker" ? "docker" : "none";
|
|
13910
14106
|
const firstBackendName = Object.keys(this.config.agent.backends)[0];
|
|
@@ -14031,7 +14227,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
14031
14227
|
const { ranked } = await this.modelRecommender(hardware);
|
|
14032
14228
|
return ranked.slice(0, top);
|
|
14033
14229
|
},
|
|
14034
|
-
listModelProposals: () => (0,
|
|
14230
|
+
listModelProposals: () => (0, import_core18.listProposals)(this.projectRoot, { status: "open", kind: "model" })
|
|
14035
14231
|
});
|
|
14036
14232
|
this.server.setRecorder(this.recorder);
|
|
14037
14233
|
this.interactionQueue.onPush((interaction) => {
|
|
@@ -14048,7 +14244,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
14048
14244
|
setupTelemetryExport(config, webhookStore, webhookDelivery) {
|
|
14049
14245
|
const otlpCfg = config.telemetry?.export?.otlp;
|
|
14050
14246
|
if (!otlpCfg) return;
|
|
14051
|
-
this.otlpExporter = new
|
|
14247
|
+
this.otlpExporter = new import_core19.OTLPExporter({
|
|
14052
14248
|
endpoint: otlpCfg.endpoint,
|
|
14053
14249
|
...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
|
|
14054
14250
|
...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
|
|
@@ -14151,7 +14347,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
14151
14347
|
...this.config.tracker.apiKey ? { token: this.config.tracker.apiKey } : {},
|
|
14152
14348
|
...this.config.tracker.endpoint ? { apiBase: this.config.tracker.endpoint } : {}
|
|
14153
14349
|
};
|
|
14154
|
-
const clientResult = (0,
|
|
14350
|
+
const clientResult = (0, import_core17.createTrackerClient)(trackerCfg);
|
|
14155
14351
|
if (!clientResult.ok) throw clientResult.error;
|
|
14156
14352
|
return new GitHubIssuesIssueTrackerAdapter(clientResult.value, this.config.tracker);
|
|
14157
14353
|
}
|
|
@@ -14730,12 +14926,12 @@ ${messages}`);
|
|
|
14730
14926
|
async postLifecycleComment(identifier, externalId, event) {
|
|
14731
14927
|
try {
|
|
14732
14928
|
if (!externalId) return;
|
|
14733
|
-
const trackerConfig = (0,
|
|
14929
|
+
const trackerConfig = (0, import_core17.loadTrackerSyncConfig)(this.projectRoot);
|
|
14734
14930
|
if (!trackerConfig) return;
|
|
14735
14931
|
const token = process.env.GITHUB_TOKEN;
|
|
14736
14932
|
if (!token) return;
|
|
14737
14933
|
const orchestratorId = await this.orchestratorIdPromise;
|
|
14738
|
-
const adapter = new
|
|
14934
|
+
const adapter = new import_core17.GitHubIssuesSyncAdapter({ token, config: trackerConfig });
|
|
14739
14935
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
14740
14936
|
const actionMap = {
|
|
14741
14937
|
claimed: "Dispatching agent for autonomous execution",
|
|
@@ -14809,7 +15005,7 @@ ${messages}`);
|
|
|
14809
15005
|
...f.line !== void 0 ? { line: f.line } : {}
|
|
14810
15006
|
}))
|
|
14811
15007
|
);
|
|
14812
|
-
(0,
|
|
15008
|
+
(0, import_core16.writeTaint)(
|
|
14813
15009
|
workspacePath,
|
|
14814
15010
|
issue.id,
|
|
14815
15011
|
"Medium-severity injection patterns found in workspace config files",
|
|
@@ -15093,6 +15289,24 @@ ${messages}`);
|
|
|
15093
15289
|
});
|
|
15094
15290
|
this.modelPool = new import_local_models4.PoolManager({ store, installer: this.modelInstaller, onWarn });
|
|
15095
15291
|
}
|
|
15292
|
+
/**
|
|
15293
|
+
* LMLM Phase 7 wiring: apply the operator's configured pool bounds (disk
|
|
15294
|
+
* budget + org/family allowlist) from `localModels.pool` to the live pool.
|
|
15295
|
+
* Called after `PoolStateStore.load()` so config wins over persisted bounds
|
|
15296
|
+
* (D2, declarative precedence). No-op when the pool is null (LMLM disabled)
|
|
15297
|
+
* or no `pool` block is configured, so it is safe to call unconditionally
|
|
15298
|
+
* on the startup path.
|
|
15299
|
+
*/
|
|
15300
|
+
async applyConfiguredPoolBounds() {
|
|
15301
|
+
const pool = this.modelPool;
|
|
15302
|
+
const bounds = this.config.localModels?.pool;
|
|
15303
|
+
if (pool === null || !bounds) return;
|
|
15304
|
+
await pool.configurePool({
|
|
15305
|
+
diskBudgetGb: bounds.diskBudgetGb,
|
|
15306
|
+
allowedOrgs: bounds.allowedOrgs,
|
|
15307
|
+
allowedFamilies: bounds.allowedFamilies
|
|
15308
|
+
});
|
|
15309
|
+
}
|
|
15096
15310
|
/**
|
|
15097
15311
|
* LMLM Phase 6: arm the single background refresh scheduler over the live
|
|
15098
15312
|
* pool. No-op when LMLM is disabled (`modelPool` null). Each tick runs
|
|
@@ -15109,7 +15323,18 @@ ${messages}`);
|
|
|
15109
15323
|
if (this.modelPool === null) return;
|
|
15110
15324
|
const pool = this.modelPool;
|
|
15111
15325
|
const refreshCfg = this.config.localModels?.refresh;
|
|
15112
|
-
const
|
|
15326
|
+
const frozen = (0, import_local_models4.loadFrozenCandidates)();
|
|
15327
|
+
const candidates = (0, import_local_models4.selectCandidates)(frozen.candidates, this.config.localModels?.pool);
|
|
15328
|
+
for (const warning of frozen.warnings) {
|
|
15329
|
+
this.logger.warn("LMLM frozen candidate snapshot degraded", { warning });
|
|
15330
|
+
}
|
|
15331
|
+
if (candidates.length === 0) {
|
|
15332
|
+
this.logger.warn("LMLM recommender has no candidates", {
|
|
15333
|
+
frozenCount: frozen.candidates.length,
|
|
15334
|
+
allowedOrgs: this.config.localModels?.pool?.allowedOrgs ?? []
|
|
15335
|
+
});
|
|
15336
|
+
}
|
|
15337
|
+
const recommend = (0, import_local_models4.createNativeRecommender)({ candidates });
|
|
15113
15338
|
this.modelRecommender = recommend;
|
|
15114
15339
|
this.refreshScheduler = new import_local_models4.RefreshScheduler({
|
|
15115
15340
|
runTick: () => (0, import_local_models4.runRefreshTick)({
|
|
@@ -15120,7 +15345,7 @@ ${messages}`);
|
|
|
15120
15345
|
// Phase 7: after persisting the proposal, emit `local-models:proposal`
|
|
15121
15346
|
// (== MODEL_PROPOSAL_TOPIC) on the bus so it fans out to WS clients and
|
|
15122
15347
|
// notification sinks. Literal to avoid a proposals/model-handlers cycle.
|
|
15123
|
-
emitProposal: (c) => (0,
|
|
15348
|
+
emitProposal: (c) => (0, import_core18.createModelProposal)(this.projectRoot, c).then((record) => {
|
|
15124
15349
|
this.emit("local-models:proposal", {
|
|
15125
15350
|
id: record.id,
|
|
15126
15351
|
status: "created",
|
|
@@ -15148,7 +15373,7 @@ ${messages}`);
|
|
|
15148
15373
|
}
|
|
15149
15374
|
/** Map the on-disk model-proposal queue to F7 dedup pairs (open→pending, rejected→rejected). */
|
|
15150
15375
|
async lmlmDedupSource() {
|
|
15151
|
-
const proposals = await (0,
|
|
15376
|
+
const proposals = await (0, import_core18.listProposals)(this.projectRoot, { kind: "model" });
|
|
15152
15377
|
const pending = [];
|
|
15153
15378
|
const rejected = [];
|
|
15154
15379
|
for (const p of proposals) {
|
|
@@ -15186,6 +15411,7 @@ ${messages}`);
|
|
|
15186
15411
|
}
|
|
15187
15412
|
if (this.poolStateStore !== null) {
|
|
15188
15413
|
await this.poolStateStore.load();
|
|
15414
|
+
await this.applyConfiguredPoolBounds();
|
|
15189
15415
|
}
|
|
15190
15416
|
for (const resolver of this.localResolvers.values()) {
|
|
15191
15417
|
await resolver.start();
|
package/dist/index.mjs
CHANGED
|
@@ -3976,9 +3976,11 @@ import {
|
|
|
3976
3976
|
HardwareDetector,
|
|
3977
3977
|
RefreshScheduler,
|
|
3978
3978
|
runRefreshTick,
|
|
3979
|
-
createNativeRecommender
|
|
3979
|
+
createNativeRecommender,
|
|
3980
|
+
loadFrozenCandidates,
|
|
3981
|
+
selectCandidates as selectCandidates2
|
|
3980
3982
|
} from "@harness-engineering/local-models";
|
|
3981
|
-
import { createModelProposal, listProposals as listProposals2 } from "@harness-engineering/core";
|
|
3983
|
+
import { createModelProposal as createModelProposal2, listProposals as listProposals2 } from "@harness-engineering/core";
|
|
3982
3984
|
|
|
3983
3985
|
// src/agent/config-migration.ts
|
|
3984
3986
|
var MIGRATION_GUIDE = "docs/guides/multi-backend-routing.md";
|
|
@@ -9139,17 +9141,187 @@ async function runForceRefresh(res, scheduler, deps) {
|
|
|
9139
9141
|
});
|
|
9140
9142
|
}
|
|
9141
9143
|
|
|
9144
|
+
// src/server/routes/v1/local-models-pool-mutation.ts
|
|
9145
|
+
import { createModelProposal, updateProposal as updateProposal4 } from "@harness-engineering/core";
|
|
9146
|
+
var INSTALL_RE = /^\/api\/v1\/local-models\/pool\/install(?:\?.*)?$/;
|
|
9147
|
+
var REMOVE_RE = /^\/api\/v1\/local-models\/pool\/remove(?:\?.*)?$/;
|
|
9148
|
+
var RESOLVE_TOP = 50;
|
|
9149
|
+
function sendJSON10(res, status, body) {
|
|
9150
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
9151
|
+
res.end(JSON.stringify(body));
|
|
9152
|
+
}
|
|
9153
|
+
function decidedByOf(deps, req) {
|
|
9154
|
+
if (deps.getDecidedBy) return deps.getDecidedBy(req);
|
|
9155
|
+
const token = req._authToken;
|
|
9156
|
+
return token?.id ?? "operator";
|
|
9157
|
+
}
|
|
9158
|
+
function handlerDeps(deps, req, pool) {
|
|
9159
|
+
return {
|
|
9160
|
+
pool,
|
|
9161
|
+
bus: deps.bus,
|
|
9162
|
+
updateProposal: (id, patch) => updateProposal4(deps.projectPath, id, patch),
|
|
9163
|
+
decidedBy: decidedByOf(deps, req),
|
|
9164
|
+
...deps.isModelInUse ? { isModelInUse: deps.isModelInUse } : {}
|
|
9165
|
+
};
|
|
9166
|
+
}
|
|
9167
|
+
async function readJsonBody(req) {
|
|
9168
|
+
try {
|
|
9169
|
+
const raw = await readBody(req);
|
|
9170
|
+
if (raw.length === 0) return {};
|
|
9171
|
+
const parsed = JSON.parse(raw);
|
|
9172
|
+
return typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
9173
|
+
} catch {
|
|
9174
|
+
return void 0;
|
|
9175
|
+
}
|
|
9176
|
+
}
|
|
9177
|
+
async function handleInstall(req, res, deps) {
|
|
9178
|
+
const pool = deps.getModelPool();
|
|
9179
|
+
if (pool === null) return sendJSON10(res, 503, { error: "LMLM disabled" });
|
|
9180
|
+
const body = await readJsonBody(req);
|
|
9181
|
+
if (body === void 0) return sendJSON10(res, 400, { error: "invalid JSON body" });
|
|
9182
|
+
const hfRepoId = typeof body.hfRepoId === "string" ? body.hfRepoId : "";
|
|
9183
|
+
if (hfRepoId.length === 0) return sendJSON10(res, 400, { error: "hfRepoId is required" });
|
|
9184
|
+
const quant = typeof body.quant === "string" ? body.quant : void 0;
|
|
9185
|
+
if (!deps.getRecommendations) {
|
|
9186
|
+
return sendJSON10(res, 503, { error: "recommendations unavailable" });
|
|
9187
|
+
}
|
|
9188
|
+
const ranked = await deps.getRecommendations({ top: RESOLVE_TOP, profile: "general" });
|
|
9189
|
+
const match = ranked.find(
|
|
9190
|
+
(r) => r.hfRepoId === hfRepoId && (quant === void 0 || r.quant === quant)
|
|
9191
|
+
);
|
|
9192
|
+
if (!match) {
|
|
9193
|
+
return sendJSON10(res, 404, {
|
|
9194
|
+
error: `no recommendation for ${hfRepoId}${quant ? ` @ ${quant}` : ""}`
|
|
9195
|
+
});
|
|
9196
|
+
}
|
|
9197
|
+
if (!match.ollamaName) {
|
|
9198
|
+
return sendJSON10(res, 422, { error: `${hfRepoId} has no known Ollama mirror to install` });
|
|
9199
|
+
}
|
|
9200
|
+
const content = {
|
|
9201
|
+
action: "add",
|
|
9202
|
+
target: { hfRepoId: match.hfRepoId, ollamaName: match.ollamaName },
|
|
9203
|
+
scoreDelta: match.score,
|
|
9204
|
+
justification: {
|
|
9205
|
+
summary: `Operator-initiated install of ${match.ollamaName} (${match.quant}).`,
|
|
9206
|
+
benchmarkBasis: [],
|
|
9207
|
+
hardwareFit: match.fitsHardware ? "fits detected hardware" : "may not fit detected hardware",
|
|
9208
|
+
evidence: match.evidence,
|
|
9209
|
+
freshness: `snapshot ${match.benchmarkSnapshot}`
|
|
9210
|
+
},
|
|
9211
|
+
// 0 → the installer resolves the true on-disk size via `inspect` before the
|
|
9212
|
+
// budget check, rather than trusting an estimate.
|
|
9213
|
+
diskImpactGb: 0
|
|
9214
|
+
};
|
|
9215
|
+
const record = await createModelProposal(deps.projectPath, content, {
|
|
9216
|
+
proposedBy: decidedByOf(deps, req)
|
|
9217
|
+
});
|
|
9218
|
+
const outcome = await onApproveModelProposal(handlerDeps(deps, req, pool), record);
|
|
9219
|
+
if (outcome.status === "approved") {
|
|
9220
|
+
const result = {
|
|
9221
|
+
disposition: "installed",
|
|
9222
|
+
proposalId: record.id,
|
|
9223
|
+
evicted: outcome.evicted.map((e) => e.ollamaName)
|
|
9224
|
+
};
|
|
9225
|
+
return sendJSON10(res, 200, result);
|
|
9226
|
+
}
|
|
9227
|
+
if (outcome.status === "failed_target_missing") {
|
|
9228
|
+
return sendJSON10(res, 404, {
|
|
9229
|
+
error: `${hfRepoId} is no longer available on HuggingFace`,
|
|
9230
|
+
proposalId: record.id
|
|
9231
|
+
});
|
|
9232
|
+
}
|
|
9233
|
+
const status = outcome.code === "not_allowed" || outcome.code === "budget_exceeded" ? 409 : 502;
|
|
9234
|
+
return sendJSON10(res, status, {
|
|
9235
|
+
error: outcome.message,
|
|
9236
|
+
code: outcome.code,
|
|
9237
|
+
proposalId: record.id
|
|
9238
|
+
});
|
|
9239
|
+
}
|
|
9240
|
+
async function handleRemove(req, res, deps) {
|
|
9241
|
+
const pool = deps.getModelPool();
|
|
9242
|
+
if (pool === null) return sendJSON10(res, 503, { error: "LMLM disabled" });
|
|
9243
|
+
const body = await readJsonBody(req);
|
|
9244
|
+
if (body === void 0) return sendJSON10(res, 400, { error: "invalid JSON body" });
|
|
9245
|
+
const ollamaName = typeof body.ollamaName === "string" ? body.ollamaName : "";
|
|
9246
|
+
if (ollamaName.length === 0) return sendJSON10(res, 400, { error: "ollamaName is required" });
|
|
9247
|
+
const entry = pool.snapshot().entries.find((e) => e.ollamaName === ollamaName);
|
|
9248
|
+
if (!entry) return sendJSON10(res, 404, { error: `${ollamaName} is not in the pool` });
|
|
9249
|
+
const content = {
|
|
9250
|
+
action: "evict",
|
|
9251
|
+
target: { hfRepoId: entry.hfRepoId, ollamaName },
|
|
9252
|
+
scoreDelta: 0,
|
|
9253
|
+
justification: {
|
|
9254
|
+
summary: `Operator-initiated removal of ${ollamaName}.`,
|
|
9255
|
+
benchmarkBasis: [],
|
|
9256
|
+
hardwareFit: "",
|
|
9257
|
+
evidence: "operator",
|
|
9258
|
+
freshness: ""
|
|
9259
|
+
},
|
|
9260
|
+
diskImpactGb: entry.sizeOnDiskGb
|
|
9261
|
+
};
|
|
9262
|
+
const record = await createModelProposal(deps.projectPath, content, {
|
|
9263
|
+
proposedBy: decidedByOf(deps, req)
|
|
9264
|
+
});
|
|
9265
|
+
const outcome = await onApproveModelProposal(handlerDeps(deps, req, pool), record);
|
|
9266
|
+
if (outcome.status === "error") {
|
|
9267
|
+
return sendJSON10(res, 502, {
|
|
9268
|
+
error: outcome.message,
|
|
9269
|
+
code: outcome.code,
|
|
9270
|
+
proposalId: record.id
|
|
9271
|
+
});
|
|
9272
|
+
}
|
|
9273
|
+
if (outcome.status === "failed_target_missing") {
|
|
9274
|
+
return sendJSON10(res, 500, {
|
|
9275
|
+
error: "unexpected failed_target_missing on evict",
|
|
9276
|
+
proposalId: record.id
|
|
9277
|
+
});
|
|
9278
|
+
}
|
|
9279
|
+
if (outcome.evicted.length > 0) {
|
|
9280
|
+
const result2 = {
|
|
9281
|
+
disposition: "removed",
|
|
9282
|
+
proposalId: record.id,
|
|
9283
|
+
evicted: outcome.evicted.map((e) => e.ollamaName)
|
|
9284
|
+
};
|
|
9285
|
+
return sendJSON10(res, 200, result2);
|
|
9286
|
+
}
|
|
9287
|
+
const stillPresent = pool.snapshot().entries.some((e) => e.ollamaName === ollamaName);
|
|
9288
|
+
if (stillPresent) {
|
|
9289
|
+
const result2 = {
|
|
9290
|
+
disposition: "deferred",
|
|
9291
|
+
proposalId: record.id,
|
|
9292
|
+
evicted: [],
|
|
9293
|
+
message: "model is in use; it will be removed after the current run"
|
|
9294
|
+
};
|
|
9295
|
+
return sendJSON10(res, 202, result2);
|
|
9296
|
+
}
|
|
9297
|
+
const result = { disposition: "removed", proposalId: record.id, evicted: [] };
|
|
9298
|
+
return sendJSON10(res, 200, result);
|
|
9299
|
+
}
|
|
9300
|
+
function handleV1LocalModelsMutationRoute(req, res, deps) {
|
|
9301
|
+
const url = req.url ?? "";
|
|
9302
|
+
if ((req.method ?? "GET") !== "POST") return false;
|
|
9303
|
+
if (INSTALL_RE.test(url)) {
|
|
9304
|
+
void handleInstall(req, res, deps);
|
|
9305
|
+
return true;
|
|
9306
|
+
}
|
|
9307
|
+
if (REMOVE_RE.test(url)) {
|
|
9308
|
+
void handleRemove(req, res, deps);
|
|
9309
|
+
return true;
|
|
9310
|
+
}
|
|
9311
|
+
return false;
|
|
9312
|
+
}
|
|
9313
|
+
|
|
9142
9314
|
// src/server/routes/v1/routing.ts
|
|
9143
9315
|
import { z as z14 } from "zod";
|
|
9144
9316
|
var CONFIG_RE = /^\/api\/v1\/routing\/config(?:\?.*)?$/;
|
|
9145
9317
|
var DECISIONS_RE = /^\/api\/v1\/routing\/decisions(?:\?.*)?$/;
|
|
9146
9318
|
var TRACE_RE = /^\/api\/v1\/routing\/trace(?:\?.*)?$/;
|
|
9147
|
-
function
|
|
9319
|
+
function sendJSON11(res, status, body) {
|
|
9148
9320
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
9149
9321
|
res.end(JSON.stringify(body));
|
|
9150
9322
|
}
|
|
9151
9323
|
function unavailable(res) {
|
|
9152
|
-
|
|
9324
|
+
sendJSON11(res, 503, { error: "BackendRouter not available" });
|
|
9153
9325
|
return true;
|
|
9154
9326
|
}
|
|
9155
9327
|
function resolveChain(value, backends) {
|
|
@@ -9186,7 +9358,7 @@ function buildResolvedChains(routing, backends) {
|
|
|
9186
9358
|
}
|
|
9187
9359
|
function handleConfig(res, deps) {
|
|
9188
9360
|
if (!deps.router || !deps.routing || !deps.backends) return unavailable(res);
|
|
9189
|
-
|
|
9361
|
+
sendJSON11(res, 200, {
|
|
9190
9362
|
routing: deps.routing,
|
|
9191
9363
|
resolvedChains: buildResolvedChains(deps.routing, deps.backends),
|
|
9192
9364
|
backends: Object.keys(deps.backends)
|
|
@@ -9214,7 +9386,7 @@ function parseDecisionsQuery(url) {
|
|
|
9214
9386
|
function handleDecisions(req, res, deps) {
|
|
9215
9387
|
if (!deps.bus) return unavailable(res);
|
|
9216
9388
|
const filter = parseDecisionsQuery(req.url ?? "");
|
|
9217
|
-
|
|
9389
|
+
sendJSON11(res, 200, { decisions: deps.bus.recent(filter) });
|
|
9218
9390
|
return true;
|
|
9219
9391
|
}
|
|
9220
9392
|
var UseCaseSchema = z14.discriminatedUnion("kind", [
|
|
@@ -9246,19 +9418,19 @@ async function handleTrace(req, res, deps) {
|
|
|
9246
9418
|
try {
|
|
9247
9419
|
raw = await readBody(req);
|
|
9248
9420
|
} catch {
|
|
9249
|
-
|
|
9421
|
+
sendJSON11(res, 400, { error: "body read failed" });
|
|
9250
9422
|
return true;
|
|
9251
9423
|
}
|
|
9252
9424
|
let parsed;
|
|
9253
9425
|
try {
|
|
9254
9426
|
parsed = JSON.parse(raw);
|
|
9255
9427
|
} catch {
|
|
9256
|
-
|
|
9428
|
+
sendJSON11(res, 400, { error: "invalid JSON body" });
|
|
9257
9429
|
return true;
|
|
9258
9430
|
}
|
|
9259
9431
|
const r = TraceBodySchema.safeParse(parsed);
|
|
9260
9432
|
if (!r.success) {
|
|
9261
|
-
|
|
9433
|
+
sendJSON11(res, 400, { error: r.error.message });
|
|
9262
9434
|
return true;
|
|
9263
9435
|
}
|
|
9264
9436
|
const opts = r.data.invocationOverride !== void 0 ? { invocationOverride: r.data.invocationOverride } : void 0;
|
|
@@ -9271,9 +9443,9 @@ async function handleTrace(req, res, deps) {
|
|
|
9271
9443
|
r.data.useCase,
|
|
9272
9444
|
opts
|
|
9273
9445
|
);
|
|
9274
|
-
|
|
9446
|
+
sendJSON11(res, 200, { decision, def: { type: def.type } });
|
|
9275
9447
|
} catch (err) {
|
|
9276
|
-
|
|
9448
|
+
sendJSON11(res, 500, { error: String(err) });
|
|
9277
9449
|
}
|
|
9278
9450
|
return true;
|
|
9279
9451
|
}
|
|
@@ -9514,7 +9686,7 @@ var CreateBodySchema = z16.object({
|
|
|
9514
9686
|
tenantId: z16.string().optional(),
|
|
9515
9687
|
expiresAt: z16.string().datetime().optional()
|
|
9516
9688
|
});
|
|
9517
|
-
function
|
|
9689
|
+
function sendJSON12(res, status, body) {
|
|
9518
9690
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
9519
9691
|
res.end(JSON.stringify(body));
|
|
9520
9692
|
}
|
|
@@ -9524,19 +9696,19 @@ async function handlePost(req, res, store) {
|
|
|
9524
9696
|
raw = await readBody(req);
|
|
9525
9697
|
} catch (err) {
|
|
9526
9698
|
const msg = err instanceof Error ? err.message : "Failed to read body";
|
|
9527
|
-
|
|
9699
|
+
sendJSON12(res, 413, { error: msg });
|
|
9528
9700
|
return;
|
|
9529
9701
|
}
|
|
9530
9702
|
let json;
|
|
9531
9703
|
try {
|
|
9532
9704
|
json = JSON.parse(raw);
|
|
9533
9705
|
} catch {
|
|
9534
|
-
|
|
9706
|
+
sendJSON12(res, 400, { error: "Invalid JSON body" });
|
|
9535
9707
|
return;
|
|
9536
9708
|
}
|
|
9537
9709
|
const parsed = CreateBodySchema.safeParse(json);
|
|
9538
9710
|
if (!parsed.success) {
|
|
9539
|
-
|
|
9711
|
+
sendJSON12(res, 422, { error: "Invalid body", issues: parsed.error.issues });
|
|
9540
9712
|
return;
|
|
9541
9713
|
}
|
|
9542
9714
|
try {
|
|
@@ -9549,37 +9721,37 @@ async function handlePost(req, res, store) {
|
|
|
9549
9721
|
if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
|
|
9550
9722
|
const result = await store.create(input);
|
|
9551
9723
|
const publicRecord = AuthTokenPublicSchema.parse(result.record);
|
|
9552
|
-
|
|
9724
|
+
sendJSON12(res, 200, {
|
|
9553
9725
|
...publicRecord,
|
|
9554
9726
|
token: result.token
|
|
9555
9727
|
});
|
|
9556
9728
|
} catch (err) {
|
|
9557
9729
|
const msg = err instanceof Error ? err.message : "Failed to create token";
|
|
9558
9730
|
if (msg.includes("already exists")) {
|
|
9559
|
-
|
|
9731
|
+
sendJSON12(res, 409, { error: msg });
|
|
9560
9732
|
return;
|
|
9561
9733
|
}
|
|
9562
|
-
|
|
9734
|
+
sendJSON12(res, 500, { error: "Internal error creating token" });
|
|
9563
9735
|
}
|
|
9564
9736
|
}
|
|
9565
9737
|
async function handleList3(res, store) {
|
|
9566
9738
|
try {
|
|
9567
9739
|
const list = await store.list();
|
|
9568
|
-
|
|
9740
|
+
sendJSON12(res, 200, list);
|
|
9569
9741
|
} catch {
|
|
9570
|
-
|
|
9742
|
+
sendJSON12(res, 500, { error: "Internal error listing tokens" });
|
|
9571
9743
|
}
|
|
9572
9744
|
}
|
|
9573
9745
|
async function handleDelete2(res, store, id) {
|
|
9574
9746
|
try {
|
|
9575
9747
|
const ok = await store.revoke(id);
|
|
9576
9748
|
if (!ok) {
|
|
9577
|
-
|
|
9749
|
+
sendJSON12(res, 404, { error: "Token not found" });
|
|
9578
9750
|
return;
|
|
9579
9751
|
}
|
|
9580
|
-
|
|
9752
|
+
sendJSON12(res, 200, { deleted: true });
|
|
9581
9753
|
} catch {
|
|
9582
|
-
|
|
9754
|
+
sendJSON12(res, 500, { error: "Internal error revoking token" });
|
|
9583
9755
|
}
|
|
9584
9756
|
}
|
|
9585
9757
|
var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
|
|
@@ -9604,12 +9776,12 @@ function handleAuthRoute(req, res, store) {
|
|
|
9604
9776
|
return true;
|
|
9605
9777
|
}
|
|
9606
9778
|
}
|
|
9607
|
-
|
|
9779
|
+
sendJSON12(res, 405, { error: "Method not allowed" });
|
|
9608
9780
|
return true;
|
|
9609
9781
|
}
|
|
9610
9782
|
|
|
9611
9783
|
// src/server/routes/local-model.ts
|
|
9612
|
-
function
|
|
9784
|
+
function sendJSON13(res, status, body) {
|
|
9613
9785
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
9614
9786
|
res.end(JSON.stringify(body));
|
|
9615
9787
|
}
|
|
@@ -9617,30 +9789,30 @@ function handleLocalModelRoute(req, res, getStatus) {
|
|
|
9617
9789
|
const { method, url } = req;
|
|
9618
9790
|
if (url !== "/api/v1/local-model/status") return false;
|
|
9619
9791
|
if (method !== "GET") {
|
|
9620
|
-
|
|
9792
|
+
sendJSON13(res, 405, { error: "Method not allowed" });
|
|
9621
9793
|
return true;
|
|
9622
9794
|
}
|
|
9623
9795
|
if (!getStatus) {
|
|
9624
|
-
|
|
9796
|
+
sendJSON13(res, 503, { error: "Local backend not configured" });
|
|
9625
9797
|
return true;
|
|
9626
9798
|
}
|
|
9627
9799
|
const status = getStatus();
|
|
9628
9800
|
if (!status) {
|
|
9629
|
-
|
|
9801
|
+
sendJSON13(res, 503, { error: "Local backend not configured" });
|
|
9630
9802
|
return true;
|
|
9631
9803
|
}
|
|
9632
|
-
|
|
9804
|
+
sendJSON13(res, 200, status);
|
|
9633
9805
|
return true;
|
|
9634
9806
|
}
|
|
9635
9807
|
function handleLocalModelsRoute(req, res, getStatuses) {
|
|
9636
9808
|
const { method, url } = req;
|
|
9637
9809
|
if (url !== "/api/v1/local-models/status") return false;
|
|
9638
9810
|
if (method !== "GET") {
|
|
9639
|
-
|
|
9811
|
+
sendJSON13(res, 405, { error: "Method not allowed" });
|
|
9640
9812
|
return true;
|
|
9641
9813
|
}
|
|
9642
9814
|
const statuses = getStatuses ? getStatuses() : [];
|
|
9643
|
-
|
|
9815
|
+
sendJSON13(res, 200, statuses);
|
|
9644
9816
|
return true;
|
|
9645
9817
|
}
|
|
9646
9818
|
|
|
@@ -10016,6 +10188,21 @@ var V1_BRIDGE_ROUTES = [
|
|
|
10016
10188
|
scope: "manage-proposals",
|
|
10017
10189
|
description: "Force a background-scheduler refresh tick and return emitted proposals (O4)."
|
|
10018
10190
|
},
|
|
10191
|
+
// ── LMLM dashboard pool mutation — operator-initiated install/remove ──
|
|
10192
|
+
// Modeled as auto-approved model proposals, so the same `manage-proposals`
|
|
10193
|
+
// write scope that governs approve/reject governs these too.
|
|
10194
|
+
{
|
|
10195
|
+
method: "POST",
|
|
10196
|
+
pattern: /^\/api\/v1\/local-models\/pool\/install(?:\?.*)?$/,
|
|
10197
|
+
scope: "manage-proposals",
|
|
10198
|
+
description: "Install a recommended model into the local pool (auto-approved proposal)."
|
|
10199
|
+
},
|
|
10200
|
+
{
|
|
10201
|
+
method: "POST",
|
|
10202
|
+
pattern: /^\/api\/v1\/local-models\/pool\/remove(?:\?.*)?$/,
|
|
10203
|
+
scope: "manage-proposals",
|
|
10204
|
+
description: "Remove a model from the local pool (auto-approved proposal)."
|
|
10205
|
+
},
|
|
10019
10206
|
// ── LMLM Phase 7 — read surface (hardware/pool/recommendations/proposals) ──
|
|
10020
10207
|
// Load-bearing: `local-models` is in `V1_WRAPPABLE`, so without these entries
|
|
10021
10208
|
// the `/api/v1` rewrite shim would rewrite `/api/v1/local-models/<name>` →
|
|
@@ -10450,6 +10637,17 @@ var OrchestratorServer = class {
|
|
|
10450
10637
|
...this.isModelInUseFn ? { isModelInUse: this.isModelInUseFn } : {}
|
|
10451
10638
|
});
|
|
10452
10639
|
},
|
|
10640
|
+
// LMLM dashboard pool mutation — POST /pool/install + /pool/remove.
|
|
10641
|
+
// Operator-initiated install/remove via auto-approved proposals; reuses
|
|
10642
|
+
// the same mutation pool + bus + in-use probe the proposals route uses.
|
|
10643
|
+
// Registered before the read surface so /pool/install|remove match first.
|
|
10644
|
+
(req, res) => handleV1LocalModelsMutationRoute(req, res, {
|
|
10645
|
+
projectPath: this.projectPath,
|
|
10646
|
+
bus: this.orchestrator,
|
|
10647
|
+
getModelPool: () => this.getModelPoolFn?.() ?? null,
|
|
10648
|
+
...this.getRecommendationsFn ? { getRecommendations: this.getRecommendationsFn } : {},
|
|
10649
|
+
...this.isModelInUseFn ? { isModelInUse: this.isModelInUseFn } : {}
|
|
10650
|
+
}),
|
|
10453
10651
|
// LMLM Phase 6/7 — POST /refresh + the GET read surface
|
|
10454
10652
|
// (hardware/pool/recommendations/proposals). Registered before the
|
|
10455
10653
|
// chat-proxy fallback so it owns the path. Each accessor is spread in
|
|
@@ -15061,6 +15259,24 @@ ${messages}`);
|
|
|
15061
15259
|
});
|
|
15062
15260
|
this.modelPool = new PoolManager({ store, installer: this.modelInstaller, onWarn });
|
|
15063
15261
|
}
|
|
15262
|
+
/**
|
|
15263
|
+
* LMLM Phase 7 wiring: apply the operator's configured pool bounds (disk
|
|
15264
|
+
* budget + org/family allowlist) from `localModels.pool` to the live pool.
|
|
15265
|
+
* Called after `PoolStateStore.load()` so config wins over persisted bounds
|
|
15266
|
+
* (D2, declarative precedence). No-op when the pool is null (LMLM disabled)
|
|
15267
|
+
* or no `pool` block is configured, so it is safe to call unconditionally
|
|
15268
|
+
* on the startup path.
|
|
15269
|
+
*/
|
|
15270
|
+
async applyConfiguredPoolBounds() {
|
|
15271
|
+
const pool = this.modelPool;
|
|
15272
|
+
const bounds = this.config.localModels?.pool;
|
|
15273
|
+
if (pool === null || !bounds) return;
|
|
15274
|
+
await pool.configurePool({
|
|
15275
|
+
diskBudgetGb: bounds.diskBudgetGb,
|
|
15276
|
+
allowedOrgs: bounds.allowedOrgs,
|
|
15277
|
+
allowedFamilies: bounds.allowedFamilies
|
|
15278
|
+
});
|
|
15279
|
+
}
|
|
15064
15280
|
/**
|
|
15065
15281
|
* LMLM Phase 6: arm the single background refresh scheduler over the live
|
|
15066
15282
|
* pool. No-op when LMLM is disabled (`modelPool` null). Each tick runs
|
|
@@ -15077,7 +15293,18 @@ ${messages}`);
|
|
|
15077
15293
|
if (this.modelPool === null) return;
|
|
15078
15294
|
const pool = this.modelPool;
|
|
15079
15295
|
const refreshCfg = this.config.localModels?.refresh;
|
|
15080
|
-
const
|
|
15296
|
+
const frozen = loadFrozenCandidates();
|
|
15297
|
+
const candidates = selectCandidates2(frozen.candidates, this.config.localModels?.pool);
|
|
15298
|
+
for (const warning of frozen.warnings) {
|
|
15299
|
+
this.logger.warn("LMLM frozen candidate snapshot degraded", { warning });
|
|
15300
|
+
}
|
|
15301
|
+
if (candidates.length === 0) {
|
|
15302
|
+
this.logger.warn("LMLM recommender has no candidates", {
|
|
15303
|
+
frozenCount: frozen.candidates.length,
|
|
15304
|
+
allowedOrgs: this.config.localModels?.pool?.allowedOrgs ?? []
|
|
15305
|
+
});
|
|
15306
|
+
}
|
|
15307
|
+
const recommend = createNativeRecommender({ candidates });
|
|
15081
15308
|
this.modelRecommender = recommend;
|
|
15082
15309
|
this.refreshScheduler = new RefreshScheduler({
|
|
15083
15310
|
runTick: () => runRefreshTick({
|
|
@@ -15088,7 +15315,7 @@ ${messages}`);
|
|
|
15088
15315
|
// Phase 7: after persisting the proposal, emit `local-models:proposal`
|
|
15089
15316
|
// (== MODEL_PROPOSAL_TOPIC) on the bus so it fans out to WS clients and
|
|
15090
15317
|
// notification sinks. Literal to avoid a proposals/model-handlers cycle.
|
|
15091
|
-
emitProposal: (c) =>
|
|
15318
|
+
emitProposal: (c) => createModelProposal2(this.projectRoot, c).then((record) => {
|
|
15092
15319
|
this.emit("local-models:proposal", {
|
|
15093
15320
|
id: record.id,
|
|
15094
15321
|
status: "created",
|
|
@@ -15154,6 +15381,7 @@ ${messages}`);
|
|
|
15154
15381
|
}
|
|
15155
15382
|
if (this.poolStateStore !== null) {
|
|
15156
15383
|
await this.poolStateStore.load();
|
|
15384
|
+
await this.applyConfiguredPoolBounds();
|
|
15157
15385
|
}
|
|
15158
15386
|
for (const resolver of this.localResolvers.values()) {
|
|
15159
15387
|
await resolver.start();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harness-engineering/orchestrator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Orchestrator daemon for dispatching coding agents to issues",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -37,22 +37,22 @@
|
|
|
37
37
|
"homepage": "https://github.com/Intense-Visions/harness-engineering/tree/main/packages/orchestrator#readme",
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@anthropic-ai/sdk": "^0.95.1",
|
|
40
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
40
|
+
"@earendil-works/pi-coding-agent": "^0.79.0",
|
|
41
41
|
"@google/genai": "^2.0.4",
|
|
42
42
|
"bcryptjs": "^2.4.3",
|
|
43
43
|
"better-sqlite3": "^12.10.0",
|
|
44
44
|
"ink": "^4.4.1",
|
|
45
|
-
"liquidjs": "^10.
|
|
45
|
+
"liquidjs": "^10.26.0",
|
|
46
46
|
"openai": "^6.0.0",
|
|
47
47
|
"react": "^18.3.1",
|
|
48
48
|
"ws": "^8.21.0",
|
|
49
49
|
"yaml": "^2.8.3",
|
|
50
50
|
"zod": "^3.25.76",
|
|
51
|
-
"@harness-engineering/core": "0.34.
|
|
52
|
-
"@harness-engineering/graph": "0.11.
|
|
53
|
-
"@harness-engineering/intelligence": "0.4.
|
|
54
|
-
"@harness-engineering/local-models": "0.
|
|
55
|
-
"@harness-engineering/types": "0.
|
|
51
|
+
"@harness-engineering/core": "0.34.1",
|
|
52
|
+
"@harness-engineering/graph": "0.11.5",
|
|
53
|
+
"@harness-engineering/intelligence": "0.4.3",
|
|
54
|
+
"@harness-engineering/local-models": "0.4.0",
|
|
55
|
+
"@harness-engineering/types": "0.19.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@asteasolutions/zod-to-openapi": "^7.3.0",
|