autopilot-ai 1.19.0 → 1.19.2
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.js +1638 -204
- package/dist/index.js.map +1 -1
- package/dist/sdk/index.js +32 -7
- package/dist/sdk/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4899,8 +4899,8 @@ var init_subagent_policy = __esm({
|
|
|
4899
4899
|
"src/intent/subagent-policy.ts"() {
|
|
4900
4900
|
"use strict";
|
|
4901
4901
|
init_jev();
|
|
4902
|
-
EXPLICIT_NO = /\b(?:do\s+not|don't|dont|never|avoid|without|no)\s+(?:(?:(?:want\s+to\s+)?(?:use|using|spawn|call|ask|start|launch|skip))\s+)?(?:(?:any|the|a)\s+)?(?:sub[- ]?agents?|agents?|workers?)\b|\b(?:do\s+not|don't|dont|never|avoid|without|no)\s+delegat(?:e|ion|ing)\b|\bwork\s+sequentially\b/i;
|
|
4903
|
-
EXPLICIT_YES = /\b(?:use|spawn|call|start|launch|ask|send|delegate\s+to)\s+(?:(?:some|several|multiple|parallel)\s+)?(?:sub[- ]?agents?|agents?|workers?)\b|\b(?:parallelize|split)\b.{0,50}\b(?:agents?|workers?|subtasks?)\b/i;
|
|
4902
|
+
EXPLICIT_NO = /\b(?:do\s+not|don't|dont|never|avoid|without|no)\s+(?:(?:(?:want\s+to\s+)?(?:use|using|spawn|call|ask|start|launch|skip|run|spin(?:\s+up)?))\s+)?(?:(?:any|the|a|some|several|multiple|parallel|\d+|one|two|three|four|five|six|seven|eight|nine|ten)\s+)?(?:sub[- ]?agents?|agents?|workers?)\b|\b(?:do\s+not|don't|dont|never|avoid|without|no)\s+delegat(?:e|ion|ing)\b|\bwork\s+sequentially\b/i;
|
|
4903
|
+
EXPLICIT_YES = /\b(?:use|spawn|call|start|launch|ask|send|run|spin(?:\s+up)?|delegate\s+to)\s+(?:(?:some|several|multiple|parallel|\d+|one|two|three|four|five|six|seven|eight|nine|ten)\s+)?(?:sub[- ]?agents?|agents?|workers?)\b|\b(?:parallelize|split)\b.{0,50}\b(?:agents?|workers?|subtasks?)\b/i;
|
|
4904
4904
|
SEQUENTIAL_DEPENDENCY = /\b(?:one\s+at\s+a\s+time|sequentially|in\s+sequence|in\s+dependency\s+order|tightly\s+coupled|same\s+(?:file|function|module)|first\b.{0,80}\bbefore\b|before\b.{0,80}\bthen\b|step[- ]by[- ]step)\b/i;
|
|
4905
4905
|
CLEAR_PARALLEL = /\b(?:independent\s+(?:research|tasks?|questions?|work)|in\s+parallel|parallel\s+agents?|separately\s+investigate)\b/i;
|
|
4906
4906
|
AMBIGUOUS_CANDIDATE = /\b(?:research|investigat\w*|audit|compare|contrast|review|comprehensive|across\s+(?:the\s+)?(?:codebase|repo|repository)|entire\s+(?:codebase|repo|repository)|multiple\s+(?:systems|modules|approaches|sources))\b/i;
|
|
@@ -13410,10 +13410,11 @@ async function loadAsterServerConfig(configPath3 = process.env.AUTOPILOT_ASTER_C
|
|
|
13410
13410
|
}
|
|
13411
13411
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new AsterConfigError();
|
|
13412
13412
|
const record = raw;
|
|
13413
|
-
|
|
13413
|
+
const modelCatalog = record.models === "openrouter" ? "openrouter" : "fixed";
|
|
13414
|
+
if (modelCatalog === "fixed" && (!Array.isArray(record.models) || record.models.length === 0 || record.models.length > 100)) {
|
|
13414
13415
|
throw new AsterConfigError();
|
|
13415
13416
|
}
|
|
13416
|
-
const models = [...new Set(record.models)];
|
|
13417
|
+
const models = modelCatalog === "fixed" ? [...new Set(record.models)] : [];
|
|
13417
13418
|
if (models.some((model) => typeof model !== "string" || !model.trim() || model.length > 200)) {
|
|
13418
13419
|
throw new AsterConfigError();
|
|
13419
13420
|
}
|
|
@@ -13451,7 +13452,7 @@ async function loadAsterServerConfig(configPath3 = process.env.AUTOPILOT_ASTER_C
|
|
|
13451
13452
|
roots.push(rootPath);
|
|
13452
13453
|
workspaces.push({ id: workspace.id, displayName: workspace.displayName.trim(), rootPath });
|
|
13453
13454
|
}
|
|
13454
|
-
return { models, workspaces, approvalTtlMs };
|
|
13455
|
+
return { models, modelCatalog, workspaces, approvalTtlMs };
|
|
13455
13456
|
}
|
|
13456
13457
|
function findAsterWorkspace(config2, workspaceId) {
|
|
13457
13458
|
return config2.workspaces.find((workspace) => workspace.id === workspaceId);
|
|
@@ -13476,7 +13477,8 @@ var init_aster_workspaces = __esm({
|
|
|
13476
13477
|
// src/server/aster-store.ts
|
|
13477
13478
|
var aster_store_exports = {};
|
|
13478
13479
|
__export(aster_store_exports, {
|
|
13479
|
-
AsterStore: () => AsterStore
|
|
13480
|
+
AsterStore: () => AsterStore,
|
|
13481
|
+
defaultAsterDbPath: () => defaultAsterDbPath
|
|
13480
13482
|
});
|
|
13481
13483
|
import Database3 from "better-sqlite3";
|
|
13482
13484
|
import { createHash as createHash3, randomBytes as randomBytes2, randomUUID as randomUUID5 } from "crypto";
|
|
@@ -13641,6 +13643,20 @@ var init_aster_store = __esm({
|
|
|
13641
13643
|
UNIQUE(credential_id, create_key_hash)
|
|
13642
13644
|
);
|
|
13643
13645
|
CREATE INDEX IF NOT EXISTS idx_aster_cells_lifecycle ON aster_cell_mappings(status, updated_at);
|
|
13646
|
+
CREATE TABLE IF NOT EXISTS aster_github_grants (
|
|
13647
|
+
conversation_id TEXT PRIMARY KEY,
|
|
13648
|
+
token TEXT NOT NULL UNIQUE,
|
|
13649
|
+
repository TEXT NOT NULL,
|
|
13650
|
+
created_at INTEGER NOT NULL,
|
|
13651
|
+
revoked_at INTEGER
|
|
13652
|
+
);
|
|
13653
|
+
CREATE TABLE IF NOT EXISTS aster_conversation_projects (
|
|
13654
|
+
conversation_id TEXT PRIMARY KEY,
|
|
13655
|
+
kind TEXT NOT NULL,
|
|
13656
|
+
repository TEXT,
|
|
13657
|
+
branch TEXT,
|
|
13658
|
+
created_at INTEGER NOT NULL
|
|
13659
|
+
);
|
|
13644
13660
|
`);
|
|
13645
13661
|
try {
|
|
13646
13662
|
chmodSync2(dbPath, 384);
|
|
@@ -13818,6 +13834,47 @@ var init_aster_store = __esm({
|
|
|
13818
13834
|
});
|
|
13819
13835
|
return update.immediate();
|
|
13820
13836
|
}
|
|
13837
|
+
/** Records a conversation's project once; the first value wins so retries can't change it. */
|
|
13838
|
+
setConversationProject(conversationId, project) {
|
|
13839
|
+
this.db.prepare(`INSERT OR IGNORE INTO aster_conversation_projects (conversation_id, kind, repository, branch, created_at)
|
|
13840
|
+
VALUES (?, ?, ?, ?, ?)`).run(conversationId, project.kind, project.repository, project.branch, Date.now());
|
|
13841
|
+
}
|
|
13842
|
+
getConversationProject(conversationId) {
|
|
13843
|
+
const row = this.db.prepare("SELECT kind, repository, branch FROM aster_conversation_projects WHERE conversation_id = ?").get(conversationId);
|
|
13844
|
+
if (!row) return void 0;
|
|
13845
|
+
return { kind: row.kind === "code" ? "code" : "chat", repository: row.repository, branch: row.branch };
|
|
13846
|
+
}
|
|
13847
|
+
/**
|
|
13848
|
+
* The repository-scoped GitHub token for a code conversation, created once. It is stored
|
|
13849
|
+
* as-is (like the server's own GitHub token, it never leaves the server except into that
|
|
13850
|
+
* conversation's cell) so provisioning retries hand the cell the same value.
|
|
13851
|
+
*/
|
|
13852
|
+
getOrCreateGitHubGrant(conversationId, repository) {
|
|
13853
|
+
const existing = this.db.prepare("SELECT token FROM aster_github_grants WHERE conversation_id = ? AND revoked_at IS NULL").get(conversationId);
|
|
13854
|
+
if (existing) return existing.token;
|
|
13855
|
+
const token = "aghp_" + randomBytes2(32).toString("base64url");
|
|
13856
|
+
this.db.prepare(`INSERT INTO aster_github_grants (conversation_id, token, repository, created_at) VALUES (?, ?, ?, ?)
|
|
13857
|
+
ON CONFLICT(conversation_id) DO UPDATE SET token = excluded.token, repository = excluded.repository, created_at = excluded.created_at, revoked_at = NULL`).run(conversationId, token, repository, Date.now());
|
|
13858
|
+
return token;
|
|
13859
|
+
}
|
|
13860
|
+
resolveGitHubGrant(token) {
|
|
13861
|
+
const row = this.db.prepare("SELECT conversation_id, repository FROM aster_github_grants WHERE token = ? AND revoked_at IS NULL").get(token);
|
|
13862
|
+
return row ? { conversationId: row.conversation_id, repository: row.repository } : void 0;
|
|
13863
|
+
}
|
|
13864
|
+
revokeGitHubGrant(conversationId) {
|
|
13865
|
+
this.db.prepare("UPDATE aster_github_grants SET revoked_at = ? WHERE conversation_id = ? AND revoked_at IS NULL").run(Date.now(), conversationId);
|
|
13866
|
+
}
|
|
13867
|
+
/** Switch an idle conversation's model; refused while a turn is active. */
|
|
13868
|
+
setConversationModel(conversationId, model) {
|
|
13869
|
+
const update = this.db.transaction(() => {
|
|
13870
|
+
const now2 = Date.now();
|
|
13871
|
+
const result2 = this.db.prepare("UPDATE aster_conversations SET model = ?, updated_at = ? WHERE id = ? AND active_run_id IS NULL").run(model, now2, conversationId);
|
|
13872
|
+
if (result2.changes !== 1) return false;
|
|
13873
|
+
this.insertEvent(conversationId, null, "model.changed", { model }, now2);
|
|
13874
|
+
return true;
|
|
13875
|
+
});
|
|
13876
|
+
return update.immediate();
|
|
13877
|
+
}
|
|
13821
13878
|
appendEvent(conversationId, runId, type, data) {
|
|
13822
13879
|
const append = this.db.transaction(() => this.insertEvent(conversationId, runId, type, data, Date.now()));
|
|
13823
13880
|
return append.immediate();
|
|
@@ -14720,6 +14777,217 @@ var init_wake_scheduler = __esm({
|
|
|
14720
14777
|
}
|
|
14721
14778
|
});
|
|
14722
14779
|
|
|
14780
|
+
// src/models/openrouter-catalog.ts
|
|
14781
|
+
var openrouter_catalog_exports = {};
|
|
14782
|
+
__export(openrouter_catalog_exports, {
|
|
14783
|
+
DEFAULT_CACHE_TTL_MS: () => DEFAULT_CACHE_TTL_MS,
|
|
14784
|
+
ensureOpenRouterCatalog: () => ensureOpenRouterCatalog,
|
|
14785
|
+
fetchOpenRouterCatalog: () => fetchOpenRouterCatalog,
|
|
14786
|
+
loadOpenRouterCatalog: () => loadOpenRouterCatalog,
|
|
14787
|
+
mapOpenRouterModel: () => mapOpenRouterModel
|
|
14788
|
+
});
|
|
14789
|
+
import { readFile as readFile20, writeFile as writeFile12, mkdir as mkdir13 } from "fs/promises";
|
|
14790
|
+
import { homedir as homedir22 } from "os";
|
|
14791
|
+
import { join as join30, dirname as dirname15 } from "path";
|
|
14792
|
+
function catalogCachePath() {
|
|
14793
|
+
const xdg = process.env.XDG_CONFIG_HOME || join30(homedir22(), ".config");
|
|
14794
|
+
return join30(xdg, "kimiflare", "openrouter-models.json");
|
|
14795
|
+
}
|
|
14796
|
+
function perMtok(perToken) {
|
|
14797
|
+
const n = Number(perToken);
|
|
14798
|
+
return Number.isFinite(n) ? n * 1e6 : 0;
|
|
14799
|
+
}
|
|
14800
|
+
function mapOpenRouterModel(raw) {
|
|
14801
|
+
const supported = new Set(raw.supported_parameters ?? []);
|
|
14802
|
+
const aa = raw.benchmarks?.artificial_analysis;
|
|
14803
|
+
const quality = {
|
|
14804
|
+
...typeof aa?.coding_index === "number" ? { coding: aa.coding_index } : {},
|
|
14805
|
+
...typeof aa?.agentic_index === "number" ? { agentic: aa.agentic_index } : {},
|
|
14806
|
+
...typeof aa?.intelligence_index === "number" ? { intelligence: aa.intelligence_index } : {}
|
|
14807
|
+
};
|
|
14808
|
+
return {
|
|
14809
|
+
id: raw.id,
|
|
14810
|
+
...raw.name ? { name: raw.name } : {},
|
|
14811
|
+
...typeof raw.created === "number" ? { created: raw.created } : {},
|
|
14812
|
+
...Object.keys(quality).length > 0 ? { quality } : {},
|
|
14813
|
+
...raw.supported_parameters ? { parameters: [...raw.supported_parameters] } : {},
|
|
14814
|
+
contextWindow: raw.context_length ?? 128e3,
|
|
14815
|
+
maxOutputTokens: raw.top_provider?.max_completion_tokens ?? 4096,
|
|
14816
|
+
pricing: {
|
|
14817
|
+
inputPerMtok: perMtok(raw.pricing?.prompt),
|
|
14818
|
+
...raw.pricing?.input_cache_read ? { cachedInputPerMtok: perMtok(raw.pricing.input_cache_read) } : {},
|
|
14819
|
+
outputPerMtok: perMtok(raw.pricing?.completion)
|
|
14820
|
+
},
|
|
14821
|
+
supports: {
|
|
14822
|
+
// OpenRouter's own catalog is the authority on whether a model accepts tool
|
|
14823
|
+
// calls; absence of "tools" here means the upstream provider doesn't support
|
|
14824
|
+
// function calling for this model, and we must not claim otherwise.
|
|
14825
|
+
tools: supported.has("tools"),
|
|
14826
|
+
reasoning: supported.has("reasoning") || supported.has("include_reasoning"),
|
|
14827
|
+
streaming: true,
|
|
14828
|
+
vision: (raw.architecture?.input_modalities ?? []).includes("image"),
|
|
14829
|
+
temperature: supported.has("temperature") ? true : void 0
|
|
14830
|
+
}
|
|
14831
|
+
};
|
|
14832
|
+
}
|
|
14833
|
+
async function fetchOpenRouterCatalog(fetchImpl2 = fetch) {
|
|
14834
|
+
const res = await fetchWithNetworkRetry(fetchImpl2, openRouterUrl("models"), {
|
|
14835
|
+
headers: { Accept: "application/json" }
|
|
14836
|
+
});
|
|
14837
|
+
if (!res.ok) {
|
|
14838
|
+
throw new Error(`OpenRouter /models returned HTTP ${res.status}`);
|
|
14839
|
+
}
|
|
14840
|
+
const body = await res.json();
|
|
14841
|
+
return (body.data ?? []).map(mapOpenRouterModel);
|
|
14842
|
+
}
|
|
14843
|
+
async function readCache3() {
|
|
14844
|
+
try {
|
|
14845
|
+
const raw = await readFile20(catalogCachePath(), "utf8");
|
|
14846
|
+
const parsed = JSON.parse(raw);
|
|
14847
|
+
return parsed.version === CACHE_VERSION ? parsed : null;
|
|
14848
|
+
} catch {
|
|
14849
|
+
return null;
|
|
14850
|
+
}
|
|
14851
|
+
}
|
|
14852
|
+
async function writeCache3(models) {
|
|
14853
|
+
const path = catalogCachePath();
|
|
14854
|
+
try {
|
|
14855
|
+
await mkdir13(dirname15(path), { recursive: true });
|
|
14856
|
+
const cache = { version: CACHE_VERSION, fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), models };
|
|
14857
|
+
await writeFile12(path, JSON.stringify(cache, null, 2), "utf8");
|
|
14858
|
+
} catch {
|
|
14859
|
+
}
|
|
14860
|
+
}
|
|
14861
|
+
async function loadOpenRouterCatalog(opts2 = {}) {
|
|
14862
|
+
const ttlMs = opts2.ttlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
14863
|
+
const cached = await readCache3();
|
|
14864
|
+
const isFresh = cached && Date.now() - Date.parse(cached.fetchedAt) < ttlMs;
|
|
14865
|
+
if (isFresh) return cached.models;
|
|
14866
|
+
try {
|
|
14867
|
+
const models = await fetchOpenRouterCatalog(opts2.fetchImpl ?? fetch);
|
|
14868
|
+
await writeCache3(models);
|
|
14869
|
+
return models;
|
|
14870
|
+
} catch {
|
|
14871
|
+
return cached?.models ?? [];
|
|
14872
|
+
}
|
|
14873
|
+
}
|
|
14874
|
+
async function ensureOpenRouterCatalog(opts2 = {}) {
|
|
14875
|
+
try {
|
|
14876
|
+
const models = await loadOpenRouterCatalog(opts2);
|
|
14877
|
+
if (models.length > 0) registerOpenRouterModels(models);
|
|
14878
|
+
return models.length;
|
|
14879
|
+
} catch {
|
|
14880
|
+
return 0;
|
|
14881
|
+
}
|
|
14882
|
+
}
|
|
14883
|
+
var CACHE_VERSION, DEFAULT_CACHE_TTL_MS;
|
|
14884
|
+
var init_openrouter_catalog = __esm({
|
|
14885
|
+
"src/models/openrouter-catalog.ts"() {
|
|
14886
|
+
"use strict";
|
|
14887
|
+
init_registry();
|
|
14888
|
+
init_openrouter();
|
|
14889
|
+
CACHE_VERSION = 4;
|
|
14890
|
+
DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
14891
|
+
}
|
|
14892
|
+
});
|
|
14893
|
+
|
|
14894
|
+
// src/server/aster-github.ts
|
|
14895
|
+
function mapRepository(raw) {
|
|
14896
|
+
return {
|
|
14897
|
+
fullName: raw.full_name,
|
|
14898
|
+
name: raw.name,
|
|
14899
|
+
owner: raw.owner?.login ?? raw.full_name.split("/")[0] ?? "",
|
|
14900
|
+
private: raw.private,
|
|
14901
|
+
description: raw.description,
|
|
14902
|
+
defaultBranch: raw.default_branch ?? "main",
|
|
14903
|
+
updatedAt: raw.pushed_at ?? raw.updated_at ?? null
|
|
14904
|
+
};
|
|
14905
|
+
}
|
|
14906
|
+
var AsterGitHubError, REPOSITORY_FULL_NAME_RE, REPOSITORY_NAME_RE, API, AsterGitHub;
|
|
14907
|
+
var init_aster_github = __esm({
|
|
14908
|
+
"src/server/aster-github.ts"() {
|
|
14909
|
+
"use strict";
|
|
14910
|
+
AsterGitHubError = class extends Error {
|
|
14911
|
+
constructor(message2, status, code) {
|
|
14912
|
+
super(message2);
|
|
14913
|
+
this.status = status;
|
|
14914
|
+
this.code = code;
|
|
14915
|
+
this.name = "AsterGitHubError";
|
|
14916
|
+
}
|
|
14917
|
+
status;
|
|
14918
|
+
code;
|
|
14919
|
+
};
|
|
14920
|
+
REPOSITORY_FULL_NAME_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]{1,100}$/;
|
|
14921
|
+
REPOSITORY_NAME_RE = /^[A-Za-z0-9._-]{1,100}$/;
|
|
14922
|
+
API = "https://api.github.com";
|
|
14923
|
+
AsterGitHub = class {
|
|
14924
|
+
constructor(token = process.env.GITHUB_TOKEN, fetchImpl2 = fetch) {
|
|
14925
|
+
this.token = token;
|
|
14926
|
+
this.fetchImpl = fetchImpl2;
|
|
14927
|
+
}
|
|
14928
|
+
token;
|
|
14929
|
+
fetchImpl;
|
|
14930
|
+
get configured() {
|
|
14931
|
+
return Boolean(this.token?.trim());
|
|
14932
|
+
}
|
|
14933
|
+
/** Repositories the token can push to, most recently pushed first. */
|
|
14934
|
+
async listRepositories(query = "") {
|
|
14935
|
+
const raw = await this.request(
|
|
14936
|
+
"GET",
|
|
14937
|
+
"/user/repos?per_page=100&sort=pushed&affiliation=owner,collaborator,organization_member"
|
|
14938
|
+
);
|
|
14939
|
+
const needle = query.trim().toLowerCase();
|
|
14940
|
+
return raw.map(mapRepository).filter((repo) => !needle || repo.fullName.toLowerCase().includes(needle) || (repo.description ?? "").toLowerCase().includes(needle));
|
|
14941
|
+
}
|
|
14942
|
+
async getRepository(fullName) {
|
|
14943
|
+
if (!REPOSITORY_FULL_NAME_RE.test(fullName)) throw new AsterGitHubError("Invalid repository name", 400, "invalid_repository");
|
|
14944
|
+
return mapRepository(await this.request("GET", `/repos/${fullName}`));
|
|
14945
|
+
}
|
|
14946
|
+
/** Creates a repository under the token's user, initialized so it has a default branch to clone. */
|
|
14947
|
+
async createRepository(input) {
|
|
14948
|
+
if (!REPOSITORY_NAME_RE.test(input.name) || input.name === "." || input.name === "..") {
|
|
14949
|
+
throw new AsterGitHubError("Repository names may use letters, digits, '.', '_' and '-'", 400, "invalid_repository_name");
|
|
14950
|
+
}
|
|
14951
|
+
const description = input.description?.trim().slice(0, 350);
|
|
14952
|
+
return mapRepository(await this.request("POST", "/user/repos", {
|
|
14953
|
+
name: input.name,
|
|
14954
|
+
private: input.private,
|
|
14955
|
+
auto_init: true,
|
|
14956
|
+
...description ? { description } : {}
|
|
14957
|
+
}));
|
|
14958
|
+
}
|
|
14959
|
+
async request(method, path, body) {
|
|
14960
|
+
if (!this.configured) throw new AsterGitHubError("GitHub is not connected on the server", 503, "github_not_configured");
|
|
14961
|
+
let response;
|
|
14962
|
+
try {
|
|
14963
|
+
response = await this.fetchImpl(API + path, {
|
|
14964
|
+
method,
|
|
14965
|
+
headers: {
|
|
14966
|
+
Accept: "application/vnd.github+json",
|
|
14967
|
+
Authorization: `Bearer ${this.token}`,
|
|
14968
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
14969
|
+
"User-Agent": "autopilot-aster",
|
|
14970
|
+
...body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
14971
|
+
},
|
|
14972
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
14973
|
+
signal: AbortSignal.timeout(2e4)
|
|
14974
|
+
});
|
|
14975
|
+
} catch {
|
|
14976
|
+
throw new AsterGitHubError("GitHub could not be reached", 502, "github_unreachable");
|
|
14977
|
+
}
|
|
14978
|
+
if (response.ok) return await response.json();
|
|
14979
|
+
const detail = await response.json().catch(() => ({}));
|
|
14980
|
+
const message2 = detail.errors?.[0]?.message ?? detail.message ?? `GitHub returned HTTP ${response.status}`;
|
|
14981
|
+
if (response.status === 404) throw new AsterGitHubError("Repository not found or not accessible with the server's GitHub token", 404, "repository_not_found");
|
|
14982
|
+
if (response.status === 401) throw new AsterGitHubError("The server's GitHub token was rejected", 502, "github_token_rejected");
|
|
14983
|
+
if (response.status === 403) throw new AsterGitHubError(`GitHub refused the request: ${message2}`, 403, "github_forbidden");
|
|
14984
|
+
if (response.status === 422) throw new AsterGitHubError(message2, 422, "github_validation_failed");
|
|
14985
|
+
throw new AsterGitHubError(message2, 502, "github_error");
|
|
14986
|
+
}
|
|
14987
|
+
};
|
|
14988
|
+
}
|
|
14989
|
+
});
|
|
14990
|
+
|
|
14723
14991
|
// src/server/aster-tools.ts
|
|
14724
14992
|
import { realpath as realpath5 } from "fs/promises";
|
|
14725
14993
|
import { isAbsolute as isAbsolute5, relative as relative7, sep as sep5 } from "path";
|
|
@@ -14785,11 +15053,18 @@ function posixNormalize(value) {
|
|
|
14785
15053
|
}
|
|
14786
15054
|
return "/" + out.join("/");
|
|
14787
15055
|
}
|
|
15056
|
+
function isLikelySkKey(candidate) {
|
|
15057
|
+
if (/^sk-or-v1-[A-Za-z0-9]{16,}/.test(candidate)) return true;
|
|
15058
|
+
return candidate.slice(3).split(/[-_]/).some((chunk) => chunk.length >= 20 && /\d/.test(chunk));
|
|
15059
|
+
}
|
|
14788
15060
|
function containsLikelyProviderSecret(value) {
|
|
14789
|
-
|
|
15061
|
+
for (const match of value.matchAll(SK_CANDIDATE_RE)) {
|
|
15062
|
+
if (isLikelySkKey(match[0])) return true;
|
|
15063
|
+
}
|
|
15064
|
+
return /(?:\bgh[pousr]_[A-Za-z0-9]{20,}|\bgithub_pat_[A-Za-z0-9_]{20,}|(?:CLOUDFLARE|OPENROUTER|REQUESTY)_API_(?:KEY|TOKEN)\s*=)/.test(value);
|
|
14790
15065
|
}
|
|
14791
15066
|
function redactLikelySecrets(value) {
|
|
14792
|
-
return value.replace(
|
|
15067
|
+
return value.replace(SK_CANDIDATE_RE, (match) => isLikelySkKey(match) ? "[REDACTED_PROVIDER_SECRET]" : match).replace(/\bgh[pousr]_[A-Za-z0-9]{20,}/g, "[REDACTED_PROVIDER_SECRET]").replace(/\bgithub_pat_[A-Za-z0-9_]{20,}/g, "[REDACTED_PROVIDER_SECRET]");
|
|
14793
15068
|
}
|
|
14794
15069
|
function assertInside(root, candidate) {
|
|
14795
15070
|
const rel = relative7(root, candidate);
|
|
@@ -14801,7 +15076,7 @@ function assertNotSensitive(root, candidate) {
|
|
|
14801
15076
|
throw new Error("workspace_sensitive_path_forbidden");
|
|
14802
15077
|
}
|
|
14803
15078
|
}
|
|
14804
|
-
var ASTR_TOOLS, SENSITIVE_NAMES;
|
|
15079
|
+
var ASTR_TOOLS, SENSITIVE_NAMES, SK_CANDIDATE_RE;
|
|
14805
15080
|
var init_aster_tools = __esm({
|
|
14806
15081
|
"src/server/aster-tools.ts"() {
|
|
14807
15082
|
"use strict";
|
|
@@ -14824,11 +15099,519 @@ var init_aster_tools = __esm({
|
|
|
14824
15099
|
"credentials",
|
|
14825
15100
|
"secrets"
|
|
14826
15101
|
]);
|
|
15102
|
+
SK_CANDIDATE_RE = /(?<![A-Za-z0-9])sk-[A-Za-z0-9_-]{16,}/g;
|
|
14827
15103
|
}
|
|
14828
15104
|
});
|
|
14829
15105
|
|
|
14830
|
-
// src/server/aster-
|
|
15106
|
+
// src/server/aster-memory.ts
|
|
15107
|
+
import Database4 from "better-sqlite3";
|
|
14831
15108
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
15109
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync7 } from "fs";
|
|
15110
|
+
import { dirname as dirname16 } from "path";
|
|
15111
|
+
function rowToItem(row) {
|
|
15112
|
+
return {
|
|
15113
|
+
id: row.id,
|
|
15114
|
+
kind: row.kind,
|
|
15115
|
+
key: row.key,
|
|
15116
|
+
title: row.title,
|
|
15117
|
+
content: row.content,
|
|
15118
|
+
pinned: row.pinned === 1,
|
|
15119
|
+
sourceConversationId: row.source_conversation_id,
|
|
15120
|
+
createdAt: row.created_at,
|
|
15121
|
+
updatedAt: row.updated_at
|
|
15122
|
+
};
|
|
15123
|
+
}
|
|
15124
|
+
function normalizeKey(kind, value) {
|
|
15125
|
+
const raw = value.toLowerCase().startsWith(`${kind}:`) ? value.slice(kind.length + 1) : value;
|
|
15126
|
+
const slug = raw.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
15127
|
+
const key = `${kind}:${slug}`;
|
|
15128
|
+
return KEY_RE.test(key) ? key : null;
|
|
15129
|
+
}
|
|
15130
|
+
function formatCard(card) {
|
|
15131
|
+
return `- ${card.title} (${card.kind}): ${card.content}`;
|
|
15132
|
+
}
|
|
15133
|
+
function mentions(text, card) {
|
|
15134
|
+
const haystack = ` ${text.toLowerCase()} `;
|
|
15135
|
+
const words = /* @__PURE__ */ new Set();
|
|
15136
|
+
for (const part of [card.title, card.key?.split(":")[1]?.replace(/-/g, " ") ?? ""]) {
|
|
15137
|
+
for (const word of part.toLowerCase().split(/[^\p{L}\p{N}]+/u)) {
|
|
15138
|
+
if (word.length >= 3 && !STOP_WORDS.has(word)) words.add(word);
|
|
15139
|
+
}
|
|
15140
|
+
}
|
|
15141
|
+
return [...words].some((word) => new RegExp(`[^\\p{L}\\p{N}]${escapeRegExp(word)}[^\\p{L}\\p{N}]`, "u").test(haystack));
|
|
15142
|
+
}
|
|
15143
|
+
function escapeRegExp(value) {
|
|
15144
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15145
|
+
}
|
|
15146
|
+
var MAX_CARD_CHARS, MAX_FACT_CHARS, MAX_MEMORY_PROMPT_ITEMS, BRIEF_FACTS, BRIEF_RECENT_SESSIONS, BRIEF_RECENT_CARDS, KEY_RE, AsterMemory, EXTRACTION_PROMPT, STOP_WORDS;
|
|
15147
|
+
var init_aster_memory = __esm({
|
|
15148
|
+
"src/server/aster-memory.ts"() {
|
|
15149
|
+
"use strict";
|
|
15150
|
+
init_openrouter();
|
|
15151
|
+
init_aster_store();
|
|
15152
|
+
init_aster_tools();
|
|
15153
|
+
MAX_CARD_CHARS = 900;
|
|
15154
|
+
MAX_FACT_CHARS = 300;
|
|
15155
|
+
MAX_MEMORY_PROMPT_ITEMS = 150;
|
|
15156
|
+
BRIEF_FACTS = 15;
|
|
15157
|
+
BRIEF_RECENT_SESSIONS = 6;
|
|
15158
|
+
BRIEF_RECENT_CARDS = 3;
|
|
15159
|
+
KEY_RE = /^(person|project):[a-z0-9][a-z0-9-]{0,80}$/;
|
|
15160
|
+
AsterMemory = class {
|
|
15161
|
+
constructor(dbPath = defaultAsterDbPath(), options = {}) {
|
|
15162
|
+
this.options = options;
|
|
15163
|
+
mkdirSync7(dirname16(dbPath), { recursive: true, mode: 448 });
|
|
15164
|
+
this.db = new Database4(dbPath);
|
|
15165
|
+
this.db.pragma("journal_mode = WAL");
|
|
15166
|
+
this.db.pragma("busy_timeout = 5000");
|
|
15167
|
+
this.db.exec(`
|
|
15168
|
+
CREATE TABLE IF NOT EXISTS aster_memories (
|
|
15169
|
+
id TEXT PRIMARY KEY,
|
|
15170
|
+
kind TEXT NOT NULL,
|
|
15171
|
+
key TEXT,
|
|
15172
|
+
title TEXT NOT NULL,
|
|
15173
|
+
content TEXT NOT NULL,
|
|
15174
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
15175
|
+
source_conversation_id TEXT,
|
|
15176
|
+
created_at INTEGER NOT NULL,
|
|
15177
|
+
updated_at INTEGER NOT NULL,
|
|
15178
|
+
deleted INTEGER NOT NULL DEFAULT 0
|
|
15179
|
+
);
|
|
15180
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_aster_memories_key ON aster_memories(key) WHERE key IS NOT NULL AND deleted = 0;
|
|
15181
|
+
CREATE INDEX IF NOT EXISTS idx_aster_memories_kind ON aster_memories(kind, deleted, updated_at);
|
|
15182
|
+
`);
|
|
15183
|
+
try {
|
|
15184
|
+
chmodSync3(dbPath, 384);
|
|
15185
|
+
} catch {
|
|
15186
|
+
}
|
|
15187
|
+
}
|
|
15188
|
+
options;
|
|
15189
|
+
db;
|
|
15190
|
+
/** Card keys already shown to the agent per conversation, so later turns only add new ones. */
|
|
15191
|
+
shown = /* @__PURE__ */ new Map();
|
|
15192
|
+
get enabled() {
|
|
15193
|
+
return this.options.enabled !== false && Boolean(this.apiKey);
|
|
15194
|
+
}
|
|
15195
|
+
get apiKey() {
|
|
15196
|
+
return this.options.apiKey ?? process.env.OPENROUTER_API_KEY;
|
|
15197
|
+
}
|
|
15198
|
+
get model() {
|
|
15199
|
+
return this.options.model ?? process.env.AUTOPILOT_ASTER_MEMORY_MODEL ?? "google/gemini-3.8-flash";
|
|
15200
|
+
}
|
|
15201
|
+
close() {
|
|
15202
|
+
this.db.close();
|
|
15203
|
+
}
|
|
15204
|
+
list(kind) {
|
|
15205
|
+
const rows = kind ? this.db.prepare("SELECT * FROM aster_memories WHERE deleted = 0 AND kind = ? ORDER BY updated_at DESC").all(kind) : this.db.prepare("SELECT * FROM aster_memories WHERE deleted = 0 ORDER BY updated_at DESC").all();
|
|
15206
|
+
return rows.map(rowToItem);
|
|
15207
|
+
}
|
|
15208
|
+
get(id) {
|
|
15209
|
+
const row = this.db.prepare("SELECT * FROM aster_memories WHERE id = ? AND deleted = 0").get(id);
|
|
15210
|
+
return row ? rowToItem(row) : void 0;
|
|
15211
|
+
}
|
|
15212
|
+
/** Soft delete; the user removed it, so extraction must not silently recreate it from this item. */
|
|
15213
|
+
delete(id) {
|
|
15214
|
+
return this.db.prepare("UPDATE aster_memories SET deleted = 1, updated_at = ? WHERE id = ? AND deleted = 0").run(Date.now(), id).changes === 1;
|
|
15215
|
+
}
|
|
15216
|
+
/** Write or merge one item. person/project/session are keyed; facts are by id. */
|
|
15217
|
+
upsert(input) {
|
|
15218
|
+
const now2 = Date.now();
|
|
15219
|
+
const limit = input.kind === "fact" ? MAX_FACT_CHARS : MAX_CARD_CHARS;
|
|
15220
|
+
const title = input.title.trim().slice(0, 120);
|
|
15221
|
+
const content = input.content.trim().slice(0, limit);
|
|
15222
|
+
const existing = input.key ? this.db.prepare("SELECT * FROM aster_memories WHERE key = ? AND deleted = 0").get(input.key) : input.id ? this.db.prepare("SELECT * FROM aster_memories WHERE id = ? AND deleted = 0").get(input.id) : void 0;
|
|
15223
|
+
if (existing) {
|
|
15224
|
+
this.db.prepare("UPDATE aster_memories SET title = ?, content = ?, pinned = ?, updated_at = ? WHERE id = ?").run(title, content, existing.pinned || input.pinned ? 1 : 0, now2, existing.id);
|
|
15225
|
+
return { item: this.get(existing.id), change: "updated" };
|
|
15226
|
+
}
|
|
15227
|
+
const id = randomUUID6();
|
|
15228
|
+
this.db.prepare(`INSERT INTO aster_memories (id, kind, key, title, content, pinned, source_conversation_id, created_at, updated_at)
|
|
15229
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.kind, input.key ?? null, title, content, input.pinned ? 1 : 0, input.sourceConversationId ?? null, now2, now2);
|
|
15230
|
+
return { item: this.get(id), change: "added" };
|
|
15231
|
+
}
|
|
15232
|
+
/**
|
|
15233
|
+
* The memory note for a turn: a full brief on a conversation's first turn, afterwards only
|
|
15234
|
+
* person/project cards the user's message newly brings up. Empty when there is nothing new.
|
|
15235
|
+
*/
|
|
15236
|
+
brief(conversationId, userText) {
|
|
15237
|
+
const items = this.list();
|
|
15238
|
+
if (items.length === 0) return "";
|
|
15239
|
+
const shown = this.shown.get(conversationId);
|
|
15240
|
+
const cards = items.filter((item) => item.kind === "person" || item.kind === "project");
|
|
15241
|
+
const mentioned = cards.filter((card) => mentions(userText, card));
|
|
15242
|
+
if (shown) {
|
|
15243
|
+
const fresh = mentioned.filter((card) => !shown.has(card.id));
|
|
15244
|
+
if (fresh.length === 0) return "";
|
|
15245
|
+
fresh.forEach((card) => shown.add(card.id));
|
|
15246
|
+
return `[Aster memory \u2014 relevant to this message]
|
|
15247
|
+
${fresh.map(formatCard).join("\n")}`;
|
|
15248
|
+
}
|
|
15249
|
+
const shownNow = /* @__PURE__ */ new Set();
|
|
15250
|
+
const facts = items.filter((item) => item.kind === "fact").sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.updatedAt - a.updatedAt).slice(0, BRIEF_FACTS);
|
|
15251
|
+
const featured = [...mentioned, ...cards.filter((card) => !mentioned.includes(card)).slice(0, BRIEF_RECENT_CARDS)];
|
|
15252
|
+
featured.forEach((card) => shownNow.add(card.id));
|
|
15253
|
+
const others = cards.filter((card) => !shownNow.has(card.id));
|
|
15254
|
+
const sessions = items.filter((item) => item.kind === "session" && item.key !== conversationId).slice(0, BRIEF_RECENT_SESSIONS);
|
|
15255
|
+
this.shown.set(conversationId, shownNow);
|
|
15256
|
+
const sections = [
|
|
15257
|
+
"[Aster memory \u2014 what you know about the user from earlier chats. Use it naturally and don't recite it. The full memory is in /workspace/.aster/memory.md; grep it when the user mentions someone or something you don't recognize.]"
|
|
15258
|
+
];
|
|
15259
|
+
if (facts.length) sections.push("About the user:\n" + facts.map((fact) => `- ${fact.content}`).join("\n"));
|
|
15260
|
+
if (featured.length) sections.push("People and projects:\n" + featured.map(formatCard).join("\n"));
|
|
15261
|
+
if (others.length) sections.push("Also known (details in memory.md): " + others.map((card) => card.title).join(", "));
|
|
15262
|
+
if (sessions.length) sections.push("Recent chats:\n" + sessions.map((session) => `- ${session.title}: ${session.content}`).join("\n"));
|
|
15263
|
+
return sections.join("\n\n");
|
|
15264
|
+
}
|
|
15265
|
+
/** All memory as Markdown, written into the cell so the agent can search it on demand. */
|
|
15266
|
+
snapshot() {
|
|
15267
|
+
const items = this.list();
|
|
15268
|
+
const group = (kind, heading, render2) => {
|
|
15269
|
+
const entries = items.filter((item) => item.kind === kind);
|
|
15270
|
+
return entries.length ? `## ${heading}
|
|
15271
|
+
|
|
15272
|
+
${entries.map(render2).join("\n")}
|
|
15273
|
+
` : "";
|
|
15274
|
+
};
|
|
15275
|
+
return [
|
|
15276
|
+
"# Aster memory\n\nWhat the user's assistant remembers across chats. Read-only here; it is updated automatically after each turn.\n",
|
|
15277
|
+
group("fact", "About the user", (item) => `- ${item.content}`),
|
|
15278
|
+
group("person", "People", (item) => `### ${item.title}
|
|
15279
|
+
${item.content}
|
|
15280
|
+
`),
|
|
15281
|
+
group("project", "Projects", (item) => `### ${item.title}
|
|
15282
|
+
${item.content}
|
|
15283
|
+
`),
|
|
15284
|
+
group("session", "Chats", (item) => `- **${item.title}** (${new Date(item.updatedAt).toISOString().slice(0, 10)}): ${item.content}`)
|
|
15285
|
+
].filter(Boolean).join("\n");
|
|
15286
|
+
}
|
|
15287
|
+
/** Read one exchange and update memory. Returns what changed (for the app's "memory updated" chip). */
|
|
15288
|
+
async remember(exchange) {
|
|
15289
|
+
if (!this.enabled) return [];
|
|
15290
|
+
const result2 = await this.extract(exchange);
|
|
15291
|
+
if (!result2) return [];
|
|
15292
|
+
const changes = [];
|
|
15293
|
+
const known = new Map(this.list().filter((item) => item.kind !== "session").map((item) => [item.id, item]));
|
|
15294
|
+
for (const id of result2.deletes ?? []) {
|
|
15295
|
+
const item = known.get(id);
|
|
15296
|
+
if (item && !item.pinned && this.delete(id)) changes.push({ id, kind: item.kind, title: item.title, change: "removed" });
|
|
15297
|
+
}
|
|
15298
|
+
for (const upsert of result2.upserts ?? []) {
|
|
15299
|
+
const kind = upsert.kind === "person" || upsert.kind === "project" || upsert.kind === "fact" ? upsert.kind : void 0;
|
|
15300
|
+
const title = upsert.title?.trim();
|
|
15301
|
+
const content = upsert.content?.trim();
|
|
15302
|
+
if (!kind || !title || !content || containsLikelyProviderSecret(content)) continue;
|
|
15303
|
+
const key = kind === "fact" ? null : normalizeKey(kind, upsert.key ?? title);
|
|
15304
|
+
if (kind !== "fact" && !key) continue;
|
|
15305
|
+
const id = kind === "fact" && upsert.id && known.has(upsert.id) ? upsert.id : null;
|
|
15306
|
+
const { item, change } = this.upsert({ kind, key, id, title, content, pinned: upsert.pinned === true, sourceConversationId: exchange.conversationId });
|
|
15307
|
+
changes.push({ id: item.id, kind, title: item.title, change });
|
|
15308
|
+
}
|
|
15309
|
+
const session = result2.session;
|
|
15310
|
+
if (session?.title?.trim() && session.summary?.trim()) {
|
|
15311
|
+
const repo = exchange.project?.kind === "code" && exchange.project.repository ? ` [${exchange.project.repository}]` : "";
|
|
15312
|
+
this.upsert({
|
|
15313
|
+
kind: "session",
|
|
15314
|
+
key: exchange.conversationId,
|
|
15315
|
+
title: session.title.trim().slice(0, 80) + repo,
|
|
15316
|
+
content: session.summary.trim(),
|
|
15317
|
+
sourceConversationId: exchange.conversationId
|
|
15318
|
+
});
|
|
15319
|
+
}
|
|
15320
|
+
return changes;
|
|
15321
|
+
}
|
|
15322
|
+
async extract(exchange) {
|
|
15323
|
+
const items = this.list().filter((item) => item.kind !== "session").sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.updatedAt - a.updatedAt).slice(0, MAX_MEMORY_PROMPT_ITEMS);
|
|
15324
|
+
const currentSession = this.list("session").find((item) => item.key === exchange.conversationId);
|
|
15325
|
+
const memory = items.length ? items.map((item) => JSON.stringify({ id: item.id, kind: item.kind, key: item.key, title: item.title, content: item.content, pinned: item.pinned })).join("\n") : "(empty)";
|
|
15326
|
+
const context = exchange.project?.kind === "code" && exchange.project.repository ? `Code chat in GitHub repository ${exchange.project.repository}.` : "General chat.";
|
|
15327
|
+
const user = [
|
|
15328
|
+
`CURRENT MEMORY (one JSON object per line):
|
|
15329
|
+
${memory}`,
|
|
15330
|
+
`THIS CHAT: ${context}${currentSession ? ` Summary so far: ${currentSession.content}` : ""}`,
|
|
15331
|
+
`LATEST USER MESSAGE:
|
|
15332
|
+
${exchange.userText.slice(0, 6e3)}`,
|
|
15333
|
+
`ASSISTANT REPLY:
|
|
15334
|
+
${exchange.assistantText.slice(0, 4e3)}`
|
|
15335
|
+
].join("\n\n");
|
|
15336
|
+
let response;
|
|
15337
|
+
try {
|
|
15338
|
+
response = await (this.options.fetchImpl ?? fetch)(openRouterUrl("chat/completions"), {
|
|
15339
|
+
method: "POST",
|
|
15340
|
+
headers: { ...openRouterHeaders(this.apiKey), "Content-Type": "application/json" },
|
|
15341
|
+
body: JSON.stringify({
|
|
15342
|
+
model: this.model,
|
|
15343
|
+
temperature: 0,
|
|
15344
|
+
response_format: { type: "json_object" },
|
|
15345
|
+
messages: [{ role: "system", content: EXTRACTION_PROMPT }, { role: "user", content: user }]
|
|
15346
|
+
}),
|
|
15347
|
+
signal: AbortSignal.timeout(3e4)
|
|
15348
|
+
});
|
|
15349
|
+
} catch {
|
|
15350
|
+
return void 0;
|
|
15351
|
+
}
|
|
15352
|
+
if (!response.ok) return void 0;
|
|
15353
|
+
const body = await response.json().catch(() => void 0);
|
|
15354
|
+
const text = body?.choices?.[0]?.message?.content;
|
|
15355
|
+
if (!text) return void 0;
|
|
15356
|
+
try {
|
|
15357
|
+
return JSON.parse(text.replace(/^```(?:json)?\s*|\s*```$/g, ""));
|
|
15358
|
+
} catch {
|
|
15359
|
+
return void 0;
|
|
15360
|
+
}
|
|
15361
|
+
}
|
|
15362
|
+
};
|
|
15363
|
+
EXTRACTION_PROMPT = `You maintain the long-term memory of one person (the user) for their AI assistant, Aster. The user has many separate chats; memory is how a new chat already knows their world. Read the latest exchange against the current memory and return JSON operations.
|
|
15364
|
+
|
|
15365
|
+
Remember what will matter in future, unrelated chats:
|
|
15366
|
+
- PEOPLE the user deals with: name, role, organization, relationship to the user, which projects they are part of, anything the user said about working with them.
|
|
15367
|
+
- PROJECTS: name, what it is, client or owner, goals, scope, stack, repository, status, key decisions, and ideas the user brainstormed or chose.
|
|
15368
|
+
- FACTS about the user: durable preferences, circumstances (job, freelance work, location if stated), ways of working, tools they use.
|
|
15369
|
+
|
|
15370
|
+
Do not store: one-off task details, things only relevant inside this chat, general knowledge, the assistant's own suggestions the user did not adopt, credentials or secrets, and sensitive personal details (health, finances, etc.) unless the user explicitly asks you to remember them.
|
|
15371
|
+
|
|
15372
|
+
Rules:
|
|
15373
|
+
- If the user explicitly asks to remember something, you MUST store it, with "pinned": true.
|
|
15374
|
+
- Prefer updating an existing card over creating a new one. To update a person or project, reuse its exact "key" and write the complete new content, merging old and new information (max ~120 words).
|
|
15375
|
+
- Keys: "person:<first-last>" or "project:<short-name>", lowercase with hyphens.
|
|
15376
|
+
- Facts are one sentence. To change a fact, return it with its "id" and new content; to remove an outdated or contradicted fact, put its id in "deletes".
|
|
15377
|
+
- Most exchanges contain nothing worth remembering: return empty "upserts" then. Never invent details.
|
|
15378
|
+
- Always write "session": a short title (max 8 words) and a 1-2 sentence summary of what this chat is about and where it stands, updated with the latest exchange.
|
|
15379
|
+
|
|
15380
|
+
Return only JSON:
|
|
15381
|
+
{"upserts":[{"kind":"person|project|fact","key":"person:...|project:...|null","id":"existing fact id|null","title":"Display name","content":"...","pinned":false}],"deletes":["id"],"session":{"title":"...","summary":"..."}}`;
|
|
15382
|
+
STOP_WORDS = /* @__PURE__ */ new Set(["the", "and", "for", "app", "project", "new", "with", "website", "site"]);
|
|
15383
|
+
}
|
|
15384
|
+
});
|
|
15385
|
+
|
|
15386
|
+
// src/server/aster-github-proxy.ts
|
|
15387
|
+
import { createServer } from "http";
|
|
15388
|
+
import { request as httpsRequest } from "https";
|
|
15389
|
+
import { request as httpRequest } from "http";
|
|
15390
|
+
function extractToken(req) {
|
|
15391
|
+
const header = req.headers.authorization;
|
|
15392
|
+
if (!header) return void 0;
|
|
15393
|
+
const [scheme, value = ""] = header.split(" ", 2);
|
|
15394
|
+
if (/^(bearer|token)$/i.test(scheme ?? "")) return value.trim() || void 0;
|
|
15395
|
+
if (/^basic$/i.test(scheme ?? "")) {
|
|
15396
|
+
const decoded = Buffer.from(value, "base64").toString("utf8");
|
|
15397
|
+
const password = decoded.slice(decoded.indexOf(":") + 1);
|
|
15398
|
+
return password || void 0;
|
|
15399
|
+
}
|
|
15400
|
+
return void 0;
|
|
15401
|
+
}
|
|
15402
|
+
function deny(res, status, message2) {
|
|
15403
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
15404
|
+
res.end(JSON.stringify({ message: message2 }));
|
|
15405
|
+
}
|
|
15406
|
+
var GIT_PATH_RE, API_PATH_RE, FORWARDED_REQUEST_HEADERS, HOP_BY_HOP, AsterGitHubProxy;
|
|
15407
|
+
var init_aster_github_proxy = __esm({
|
|
15408
|
+
"src/server/aster-github-proxy.ts"() {
|
|
15409
|
+
"use strict";
|
|
15410
|
+
GIT_PATH_RE = /^\/git\/([A-Za-z0-9-]{1,39})\/([A-Za-z0-9._-]{1,100})\.git\/(info\/refs|git-upload-pack|git-receive-pack)$/;
|
|
15411
|
+
API_PATH_RE = /^\/api\/repos\/([A-Za-z0-9-]{1,39})\/([A-Za-z0-9._-]{1,100})(\/.*)?$/;
|
|
15412
|
+
FORWARDED_REQUEST_HEADERS = ["accept", "content-type", "content-encoding", "content-length", "git-protocol", "user-agent", "x-github-api-version"];
|
|
15413
|
+
HOP_BY_HOP = /* @__PURE__ */ new Set(["connection", "keep-alive", "transfer-encoding", "upgrade", "proxy-authenticate", "proxy-authorization", "te", "trailer"]);
|
|
15414
|
+
AsterGitHubProxy = class {
|
|
15415
|
+
constructor(options) {
|
|
15416
|
+
this.options = options;
|
|
15417
|
+
this.gitUpstream = new URL(options.gitUpstream ?? "https://github.com");
|
|
15418
|
+
this.apiUpstream = new URL(options.apiUpstream ?? "https://api.github.com");
|
|
15419
|
+
}
|
|
15420
|
+
options;
|
|
15421
|
+
server;
|
|
15422
|
+
gitUpstream;
|
|
15423
|
+
apiUpstream;
|
|
15424
|
+
async listen(port, host) {
|
|
15425
|
+
this.server = createServer((req, res) => this.handle(req, res));
|
|
15426
|
+
await new Promise((resolve15, reject) => {
|
|
15427
|
+
this.server.once("error", reject);
|
|
15428
|
+
this.server.listen(port, host, () => resolve15());
|
|
15429
|
+
});
|
|
15430
|
+
const address = this.server.address();
|
|
15431
|
+
return typeof address === "object" && address ? address.port : port;
|
|
15432
|
+
}
|
|
15433
|
+
close() {
|
|
15434
|
+
this.server?.close();
|
|
15435
|
+
this.server = void 0;
|
|
15436
|
+
}
|
|
15437
|
+
handle(req, res) {
|
|
15438
|
+
const url = new URL(req.url ?? "/", "http://proxy.local");
|
|
15439
|
+
const git2 = url.pathname.match(GIT_PATH_RE);
|
|
15440
|
+
const api = git2 ? null : url.pathname.match(API_PATH_RE);
|
|
15441
|
+
if (!git2 && !api) return deny(res, 404, "Only this conversation's repository is reachable");
|
|
15442
|
+
const grantToken = extractToken(req);
|
|
15443
|
+
if (!grantToken) {
|
|
15444
|
+
res.writeHead(401, { "www-authenticate": 'Basic realm="aster-github"', "content-type": "text/plain" });
|
|
15445
|
+
res.end("credentials required");
|
|
15446
|
+
return;
|
|
15447
|
+
}
|
|
15448
|
+
const grant = this.options.resolveGrant(grantToken);
|
|
15449
|
+
if (!grant) return deny(res, 403, "This repository grant is invalid or was revoked");
|
|
15450
|
+
const [, owner, name] = git2 ?? api;
|
|
15451
|
+
if (`${owner}/${name}`.toLowerCase() !== grant.repository.toLowerCase()) {
|
|
15452
|
+
return deny(res, 403, `This conversation can only access ${grant.repository}`);
|
|
15453
|
+
}
|
|
15454
|
+
const upstream = git2 ? new URL(`/${owner}/${name}.git/${git2[3]}${url.search}`, this.gitUpstream) : new URL(`/repos/${owner}/${name}${api[3] ?? ""}${url.search}`, this.apiUpstream);
|
|
15455
|
+
const headers = {};
|
|
15456
|
+
for (const name2 of FORWARDED_REQUEST_HEADERS) {
|
|
15457
|
+
const value = req.headers[name2];
|
|
15458
|
+
if (typeof value === "string") headers[name2] = value;
|
|
15459
|
+
}
|
|
15460
|
+
headers["user-agent"] ??= "autopilot-aster";
|
|
15461
|
+
headers.authorization = git2 ? "Basic " + Buffer.from(`x-access-token:${this.options.token}`).toString("base64") : `Bearer ${this.options.token}`;
|
|
15462
|
+
const send = upstream.protocol === "https:" ? httpsRequest : httpRequest;
|
|
15463
|
+
const forward = send(upstream, { method: req.method, headers }, (upstreamRes) => {
|
|
15464
|
+
const responseHeaders = {};
|
|
15465
|
+
for (const [key, value] of Object.entries(upstreamRes.headers)) {
|
|
15466
|
+
if (value !== void 0 && !HOP_BY_HOP.has(key)) responseHeaders[key] = value;
|
|
15467
|
+
}
|
|
15468
|
+
res.writeHead(upstreamRes.statusCode ?? 502, responseHeaders);
|
|
15469
|
+
upstreamRes.pipe(res);
|
|
15470
|
+
});
|
|
15471
|
+
forward.on("error", () => {
|
|
15472
|
+
if (!res.headersSent) deny(res, 502, "GitHub could not be reached");
|
|
15473
|
+
else res.destroy();
|
|
15474
|
+
});
|
|
15475
|
+
req.pipe(forward);
|
|
15476
|
+
}
|
|
15477
|
+
};
|
|
15478
|
+
}
|
|
15479
|
+
});
|
|
15480
|
+
|
|
15481
|
+
// src/server/aster-push.ts
|
|
15482
|
+
import Database5 from "better-sqlite3";
|
|
15483
|
+
import { createSign } from "crypto";
|
|
15484
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
15485
|
+
import { connect } from "http2";
|
|
15486
|
+
function truncate2(value, limit) {
|
|
15487
|
+
const clean = value.replace(/\s+/g, " ").trim();
|
|
15488
|
+
return clean.length > limit ? clean.slice(0, limit - 1) + "\u2026" : clean;
|
|
15489
|
+
}
|
|
15490
|
+
var DEVICE_TOKEN_RE, AsterPush;
|
|
15491
|
+
var init_aster_push = __esm({
|
|
15492
|
+
"src/server/aster-push.ts"() {
|
|
15493
|
+
"use strict";
|
|
15494
|
+
init_aster_store();
|
|
15495
|
+
DEVICE_TOKEN_RE = /^[0-9a-f]{64,200}$/i;
|
|
15496
|
+
AsterPush = class {
|
|
15497
|
+
db;
|
|
15498
|
+
options;
|
|
15499
|
+
privateKey;
|
|
15500
|
+
jwt;
|
|
15501
|
+
sessions = /* @__PURE__ */ new Map();
|
|
15502
|
+
constructor(dbPath = defaultAsterDbPath(), options = {}, env2 = process.env) {
|
|
15503
|
+
this.options = {
|
|
15504
|
+
...options,
|
|
15505
|
+
keyPath: options.keyPath ?? env2.AUTOPILOT_ASTER_APNS_KEY_PATH,
|
|
15506
|
+
keyId: options.keyId ?? env2.AUTOPILOT_ASTER_APNS_KEY_ID ?? "",
|
|
15507
|
+
teamId: options.teamId ?? env2.AUTOPILOT_ASTER_APNS_TEAM_ID ?? "",
|
|
15508
|
+
topic: options.topic ?? env2.AUTOPILOT_ASTER_APNS_TOPIC ?? "com.aster.ai"
|
|
15509
|
+
};
|
|
15510
|
+
this.db = new Database5(dbPath);
|
|
15511
|
+
this.db.pragma("journal_mode = WAL");
|
|
15512
|
+
this.db.pragma("busy_timeout = 5000");
|
|
15513
|
+
this.db.exec(`
|
|
15514
|
+
CREATE TABLE IF NOT EXISTS aster_devices (
|
|
15515
|
+
token TEXT PRIMARY KEY,
|
|
15516
|
+
environment TEXT NOT NULL,
|
|
15517
|
+
created_at INTEGER NOT NULL,
|
|
15518
|
+
updated_at INTEGER NOT NULL
|
|
15519
|
+
);
|
|
15520
|
+
`);
|
|
15521
|
+
}
|
|
15522
|
+
/** Whether an APNs key is configured; devices are still recorded without one. */
|
|
15523
|
+
get configured() {
|
|
15524
|
+
return Boolean((this.options.keyPath || this.options.deliver) && this.options.keyId && this.options.teamId);
|
|
15525
|
+
}
|
|
15526
|
+
register(token, environment) {
|
|
15527
|
+
if (!DEVICE_TOKEN_RE.test(token)) return false;
|
|
15528
|
+
const now2 = Date.now();
|
|
15529
|
+
this.db.prepare(`INSERT INTO aster_devices (token, environment, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
15530
|
+
ON CONFLICT(token) DO UPDATE SET environment = excluded.environment, updated_at = excluded.updated_at`).run(token.toLowerCase(), environment, now2, now2);
|
|
15531
|
+
return true;
|
|
15532
|
+
}
|
|
15533
|
+
unregister(token) {
|
|
15534
|
+
this.db.prepare("DELETE FROM aster_devices WHERE token = ?").run(token.toLowerCase());
|
|
15535
|
+
}
|
|
15536
|
+
deviceCount() {
|
|
15537
|
+
return this.db.prepare("SELECT COUNT(*) AS n FROM aster_devices").get().n;
|
|
15538
|
+
}
|
|
15539
|
+
/** Sends to every registered device; tokens Apple reports as gone are removed. Never throws. */
|
|
15540
|
+
async notify(message2) {
|
|
15541
|
+
if (!this.configured) return;
|
|
15542
|
+
const devices = this.db.prepare("SELECT token, environment FROM aster_devices").all();
|
|
15543
|
+
if (devices.length === 0) return;
|
|
15544
|
+
const payload = JSON.stringify({
|
|
15545
|
+
aps: {
|
|
15546
|
+
alert: { title: truncate2(message2.title, 80), body: truncate2(message2.body, 180) },
|
|
15547
|
+
sound: "default",
|
|
15548
|
+
"thread-id": message2.conversationId
|
|
15549
|
+
},
|
|
15550
|
+
conversationId: message2.conversationId
|
|
15551
|
+
});
|
|
15552
|
+
await Promise.all(devices.map(async (device) => {
|
|
15553
|
+
const host = device.environment === "development" ? "api.sandbox.push.apple.com" : "api.push.apple.com";
|
|
15554
|
+
try {
|
|
15555
|
+
const status = await this.send(host, device.token, payload);
|
|
15556
|
+
if (status === 410 || status === 400) this.unregister(device.token);
|
|
15557
|
+
} catch {
|
|
15558
|
+
}
|
|
15559
|
+
}));
|
|
15560
|
+
}
|
|
15561
|
+
close() {
|
|
15562
|
+
for (const session of this.sessions.values()) session.close();
|
|
15563
|
+
this.sessions.clear();
|
|
15564
|
+
this.db.close();
|
|
15565
|
+
}
|
|
15566
|
+
async send(host, token, payload) {
|
|
15567
|
+
const headers = {
|
|
15568
|
+
authorization: `bearer ${this.token()}`,
|
|
15569
|
+
"apns-topic": this.options.topic,
|
|
15570
|
+
"apns-push-type": "alert",
|
|
15571
|
+
"apns-priority": "10"
|
|
15572
|
+
};
|
|
15573
|
+
if (this.options.deliver) return this.options.deliver({ host, token, headers, payload });
|
|
15574
|
+
const session = this.session(host);
|
|
15575
|
+
return await new Promise((resolve15, reject) => {
|
|
15576
|
+
const stream = session.request({ ":method": "POST", ":path": `/3/device/${token}`, ...headers, "content-type": "application/json" });
|
|
15577
|
+
let status = 0;
|
|
15578
|
+
stream.setTimeout(15e3, () => stream.close());
|
|
15579
|
+
stream.on("response", (responseHeaders) => {
|
|
15580
|
+
status = Number(responseHeaders[":status"]) || 0;
|
|
15581
|
+
});
|
|
15582
|
+
stream.on("error", reject);
|
|
15583
|
+
stream.on("close", () => resolve15(status));
|
|
15584
|
+
stream.resume();
|
|
15585
|
+
stream.end(payload);
|
|
15586
|
+
});
|
|
15587
|
+
}
|
|
15588
|
+
session(host) {
|
|
15589
|
+
const existing = this.sessions.get(host);
|
|
15590
|
+
if (existing && !existing.closed && !existing.destroyed) return existing;
|
|
15591
|
+
const session = connect(`https://${host}`);
|
|
15592
|
+
session.on("error", () => this.sessions.delete(host));
|
|
15593
|
+
session.on("close", () => this.sessions.delete(host));
|
|
15594
|
+
session.unref();
|
|
15595
|
+
this.sessions.set(host, session);
|
|
15596
|
+
return session;
|
|
15597
|
+
}
|
|
15598
|
+
/** Provider token (ES256 JWT); Apple accepts one for up to an hour, so it is reused for 50 minutes. */
|
|
15599
|
+
token() {
|
|
15600
|
+
const now2 = Math.floor(Date.now() / 1e3);
|
|
15601
|
+
if (this.jwt && now2 - this.jwt.issuedAt < 50 * 60) return this.jwt.value;
|
|
15602
|
+
this.privateKey ??= readFileSync5(this.options.keyPath, "utf8");
|
|
15603
|
+
const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
15604
|
+
const unsigned = `${encode({ alg: "ES256", kid: this.options.keyId })}.${encode({ iss: this.options.teamId, iat: now2 })}`;
|
|
15605
|
+
const signature = createSign("SHA256").update(unsigned).sign({ key: this.privateKey, dsaEncoding: "ieee-p1363" }).toString("base64url");
|
|
15606
|
+
this.jwt = { value: `${unsigned}.${signature}`, issuedAt: now2 };
|
|
15607
|
+
return this.jwt.value;
|
|
15608
|
+
}
|
|
15609
|
+
};
|
|
15610
|
+
}
|
|
15611
|
+
});
|
|
15612
|
+
|
|
15613
|
+
// src/server/aster-api.ts
|
|
15614
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
14832
15615
|
import { unlink as unlink2 } from "fs/promises";
|
|
14833
15616
|
import { resolve as resolve8 } from "path";
|
|
14834
15617
|
function parseCursor(header, query) {
|
|
@@ -14837,8 +15620,23 @@ function parseCursor(header, query) {
|
|
|
14837
15620
|
const cursor = Number(value);
|
|
14838
15621
|
return Number.isSafeInteger(cursor) ? cursor : void 0;
|
|
14839
15622
|
}
|
|
14840
|
-
function
|
|
15623
|
+
function collectAssistantText(store, conversationId, runId, after) {
|
|
15624
|
+
let text = "";
|
|
15625
|
+
let cursor = after;
|
|
15626
|
+
for (; ; ) {
|
|
15627
|
+
const events = store.eventsAfter(conversationId, cursor, 500);
|
|
15628
|
+
if (events.length === 0) break;
|
|
15629
|
+
for (const event of events) {
|
|
15630
|
+
if (event.runId === runId && event.type === "assistant.delta" && typeof event.data.text === "string") text += event.data.text;
|
|
15631
|
+
}
|
|
15632
|
+
cursor = events.at(-1).sequence;
|
|
15633
|
+
if (text.length > 2e4) break;
|
|
15634
|
+
}
|
|
15635
|
+
return text;
|
|
15636
|
+
}
|
|
15637
|
+
function publicConversation(conversation, project) {
|
|
14841
15638
|
return {
|
|
15639
|
+
...project ? { kind: project.kind, repository: project.repository, branch: project.branch } : {},
|
|
14842
15640
|
conversationId: conversation.id,
|
|
14843
15641
|
workspaceId: conversation.workspaceId,
|
|
14844
15642
|
model: conversation.model,
|
|
@@ -14862,6 +15660,25 @@ function publicApproval(approval) {
|
|
|
14862
15660
|
decision: approval.decision
|
|
14863
15661
|
};
|
|
14864
15662
|
}
|
|
15663
|
+
async function selectableModels(config2) {
|
|
15664
|
+
if (config2.modelCatalog === "fixed") return config2.models.map((id) => ({ id }));
|
|
15665
|
+
const catalog = await loadOpenRouterCatalog();
|
|
15666
|
+
return catalog.filter((model) => model.supports.tools).map((model) => ({
|
|
15667
|
+
id: model.id,
|
|
15668
|
+
...model.name ? { name: model.name } : {},
|
|
15669
|
+
contextWindow: model.contextWindow,
|
|
15670
|
+
inputPerMtok: model.pricing.inputPerMtok,
|
|
15671
|
+
outputPerMtok: model.pricing.outputPerMtok,
|
|
15672
|
+
...model.created !== void 0 ? { created: model.created } : {},
|
|
15673
|
+
reasoning: model.supports.reasoning,
|
|
15674
|
+
vision: model.supports.vision === true
|
|
15675
|
+
}));
|
|
15676
|
+
}
|
|
15677
|
+
async function isSelectableModel(config2, model) {
|
|
15678
|
+
if (!model.trim() || model.length > 200) return false;
|
|
15679
|
+
if (config2.modelCatalog === "fixed") return config2.models.includes(model);
|
|
15680
|
+
return (await selectableModels(config2)).some((entry) => entry.id === model);
|
|
15681
|
+
}
|
|
14865
15682
|
async function readJsonBody(req) {
|
|
14866
15683
|
const chunks = [];
|
|
14867
15684
|
let bytes = 0;
|
|
@@ -14903,10 +15720,15 @@ var init_aster_api = __esm({
|
|
|
14903
15720
|
init_version();
|
|
14904
15721
|
init_aster_store();
|
|
14905
15722
|
init_aster_workspaces();
|
|
15723
|
+
init_openrouter_catalog();
|
|
15724
|
+
init_aster_github();
|
|
15725
|
+
init_aster_memory();
|
|
15726
|
+
init_aster_github_proxy();
|
|
15727
|
+
init_aster_push();
|
|
14906
15728
|
init_aster_tools();
|
|
14907
15729
|
API_PREFIX = "/api/v1";
|
|
14908
|
-
MAX_BODY_BYTES =
|
|
14909
|
-
MAX_USER_TURN_CHARS =
|
|
15730
|
+
MAX_BODY_BYTES = 4 * 1024 * 1024;
|
|
15731
|
+
MAX_USER_TURN_CHARS = 5e5;
|
|
14910
15732
|
CLIENT_TURN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
14911
15733
|
CREATE_IDEMPOTENCY_KEY_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
14912
15734
|
MAX_APPROVAL_ARG_BYTES = 24 * 1024;
|
|
@@ -14918,9 +15740,57 @@ var init_aster_api = __esm({
|
|
|
14918
15740
|
waiters = /* @__PURE__ */ new Map();
|
|
14919
15741
|
runtime;
|
|
14920
15742
|
config;
|
|
14921
|
-
|
|
15743
|
+
github;
|
|
15744
|
+
memory;
|
|
15745
|
+
constructor(config2, runtime, github = new AsterGitHub(), memory) {
|
|
14922
15746
|
this.config = config2;
|
|
14923
15747
|
this.runtime = runtime;
|
|
15748
|
+
this.github = github;
|
|
15749
|
+
this.memory = memory;
|
|
15750
|
+
}
|
|
15751
|
+
githubProxy;
|
|
15752
|
+
/** Where cells reach the repository-scoped GitHub proxy, when it is running. */
|
|
15753
|
+
githubProxyUrl;
|
|
15754
|
+
/**
|
|
15755
|
+
* Starts the repository-scoped GitHub proxy when configured
|
|
15756
|
+
* (AUTOPILOT_ASTER_GITHUB_PROXY_LISTEN=host:port, AUTOPILOT_ASTER_GITHUB_PROXY_URL as cells see it).
|
|
15757
|
+
*/
|
|
15758
|
+
async startGitHubProxy(env2 = process.env) {
|
|
15759
|
+
const listen = env2.AUTOPILOT_ASTER_GITHUB_PROXY_LISTEN;
|
|
15760
|
+
const publicUrl = env2.AUTOPILOT_ASTER_GITHUB_PROXY_URL;
|
|
15761
|
+
const token = env2.GITHUB_TOKEN;
|
|
15762
|
+
if (!listen || !publicUrl || !token || this.githubProxy) return;
|
|
15763
|
+
const separator = listen.lastIndexOf(":");
|
|
15764
|
+
const proxy = new AsterGitHubProxy({ token, resolveGrant: (grant) => this.getStore().resolveGitHubGrant(grant) });
|
|
15765
|
+
await proxy.listen(Number(listen.slice(separator + 1)), listen.slice(0, separator));
|
|
15766
|
+
this.githubProxy = proxy;
|
|
15767
|
+
this.githubProxyUrl = publicUrl.replace(/\/+$/, "");
|
|
15768
|
+
}
|
|
15769
|
+
push;
|
|
15770
|
+
getPush() {
|
|
15771
|
+
try {
|
|
15772
|
+
return this.push ??= new AsterPush();
|
|
15773
|
+
} catch {
|
|
15774
|
+
return void 0;
|
|
15775
|
+
}
|
|
15776
|
+
}
|
|
15777
|
+
/** "Your task finished" (or failed) on the user's devices; cancelled turns stay silent. */
|
|
15778
|
+
notifyTurnEnd(store, conversationId, runId, status, reason, userText, after) {
|
|
15779
|
+
if (status !== "completed" && status !== "failed") return;
|
|
15780
|
+
const push = this.getPush();
|
|
15781
|
+
if (!push?.configured) return;
|
|
15782
|
+
const reply = status === "completed" ? collectAssistantText(store, conversationId, runId, after).trim() : "";
|
|
15783
|
+
const title = userText.trim().split("\n")[0] || "Aster";
|
|
15784
|
+
const body = status === "completed" ? reply ? `\u2705 ${reply}` : "\u2705 Done" : `\u26A0\uFE0F Couldn\u2019t finish${reason && reason !== "error" ? `: ${reason}` : ""}`;
|
|
15785
|
+
void push.notify({ title, body, conversationId });
|
|
15786
|
+
}
|
|
15787
|
+
getMemory() {
|
|
15788
|
+
if (process.env.AUTOPILOT_ASTER_MEMORY === "off") return void 0;
|
|
15789
|
+
try {
|
|
15790
|
+
return this.memory ??= new AsterMemory();
|
|
15791
|
+
} catch {
|
|
15792
|
+
return void 0;
|
|
15793
|
+
}
|
|
14924
15794
|
}
|
|
14925
15795
|
async handle(req, res) {
|
|
14926
15796
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
@@ -14942,7 +15812,7 @@ var init_aster_api = __esm({
|
|
|
14942
15812
|
service: "autopilot",
|
|
14943
15813
|
apiVersion: "v1",
|
|
14944
15814
|
serviceVersion: getAppVersion(),
|
|
14945
|
-
capabilities: ["workspaces", "conversations", "turns", "event-replay", "cancellation", "human-approvals", "hotcell-per-conversation", "pause-resume", "destroy"]
|
|
15815
|
+
capabilities: ["workspaces", "conversations", "turns", "event-replay", "cancellation", "human-approvals", "hotcell-per-conversation", "pause-resume", "destroy", "autonomous-cells", "tool-output", "checkpoints", "model-switch", "artifacts", "memory", "push", ...this.github.configured ? ["github"] : [], ...config2.modelCatalog === "openrouter" ? ["model-catalog"] : []]
|
|
14946
15816
|
});
|
|
14947
15817
|
}
|
|
14948
15818
|
if (path === `${API_PREFIX}/workspaces` && method === "GET") {
|
|
@@ -14953,27 +15823,86 @@ var init_aster_api = __esm({
|
|
|
14953
15823
|
}
|
|
14954
15824
|
if (path === `${API_PREFIX}/models` && method === "GET") {
|
|
14955
15825
|
if (!this.hasScope(principal, "models:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks model/read scope");
|
|
14956
|
-
|
|
15826
|
+
const models = await selectableModels(config2);
|
|
15827
|
+
if (models.length === 0) return sendError(res, 503, "model_catalog_unavailable", "The model catalog is unavailable; try again shortly");
|
|
15828
|
+
return json(res, 200, { models: models.map((model) => model.id), catalog: models });
|
|
14957
15829
|
}
|
|
14958
15830
|
if (path === `${API_PREFIX}/conversations` && method === "POST") {
|
|
14959
15831
|
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
14960
15832
|
const body = await readJsonBody(req);
|
|
14961
|
-
if (Object.keys(body).some((key) =>
|
|
14962
|
-
return sendError(res, 400, "unsupported_field", "Conversation creation accepts
|
|
15833
|
+
if (Object.keys(body).some((key) => !["workspaceId", "model", "kind", "repository"].includes(key))) {
|
|
15834
|
+
return sendError(res, 400, "unsupported_field", "Conversation creation accepts workspaceId, model, kind, and repository; paths are server-configured");
|
|
15835
|
+
}
|
|
15836
|
+
if (body.kind !== void 0 && body.kind !== "chat" && body.kind !== "code") {
|
|
15837
|
+
return sendError(res, 400, "invalid_kind", 'kind must be "chat" or "code"');
|
|
15838
|
+
}
|
|
15839
|
+
if (body.kind === "code" && (typeof body.repository !== "string" || !REPOSITORY_FULL_NAME_RE.test(body.repository))) {
|
|
15840
|
+
return sendError(res, 400, "repository_required", "Code conversations need a repository as owner/name");
|
|
15841
|
+
}
|
|
15842
|
+
if (body.kind !== "code" && body.repository !== void 0) {
|
|
15843
|
+
return sendError(res, 400, "unsupported_field", "repository is only accepted for code conversations");
|
|
14963
15844
|
}
|
|
14964
15845
|
if (typeof body.workspaceId !== "string" || !principal.workspaceIds.includes(body.workspaceId)) {
|
|
14965
15846
|
return sendError(res, 403, "workspace_forbidden", "Workspace ID is not in this credential's scope");
|
|
14966
15847
|
}
|
|
14967
15848
|
const workspace = findAsterWorkspace(config2, body.workspaceId);
|
|
14968
15849
|
if (!workspace) return sendError(res, 404, "workspace_not_found", "Workspace is not configured");
|
|
14969
|
-
if (typeof body.model !== "string" || !config2
|
|
15850
|
+
if (typeof body.model !== "string" || !await isSelectableModel(config2, body.model)) {
|
|
14970
15851
|
return sendError(res, 400, "model_not_allowed", "Select a model from the configured model list");
|
|
14971
15852
|
}
|
|
14972
15853
|
const idempotencyKey = req.headers["idempotency-key"];
|
|
14973
15854
|
if (typeof idempotencyKey !== "string" || !CREATE_IDEMPOTENCY_KEY_RE.test(idempotencyKey)) {
|
|
14974
15855
|
return sendError(res, 400, "idempotency_key_required", "Conversation creation requires an Idempotency-Key header of 1-128 safe characters");
|
|
14975
15856
|
}
|
|
14976
|
-
|
|
15857
|
+
const kind = body.kind;
|
|
15858
|
+
return await this.createConversation(res, store, principal, workspace, body.model, idempotencyKey, kind, body.repository);
|
|
15859
|
+
}
|
|
15860
|
+
if (path === `${API_PREFIX}/devices` && method === "POST") {
|
|
15861
|
+
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
15862
|
+
const body = await readJsonBody(req);
|
|
15863
|
+
const environment = body.environment === "development" ? "development" : "production";
|
|
15864
|
+
const push = this.getPush();
|
|
15865
|
+
if (!push || typeof body.token !== "string" || !push.register(body.token, environment)) {
|
|
15866
|
+
return sendError(res, 400, "invalid_device_token", "A hex APNs device token is required");
|
|
15867
|
+
}
|
|
15868
|
+
return json(res, 200, { registered: true, notifications: push.configured });
|
|
15869
|
+
}
|
|
15870
|
+
const deviceMatch = path.match(/^\/api\/v1\/devices\/([0-9a-fA-F]{64,200})$/);
|
|
15871
|
+
if (deviceMatch && method === "DELETE") {
|
|
15872
|
+
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
15873
|
+
this.getPush()?.unregister(deviceMatch[1]);
|
|
15874
|
+
return json(res, 200, { unregistered: true });
|
|
15875
|
+
}
|
|
15876
|
+
if (path === `${API_PREFIX}/memory` && method === "GET") {
|
|
15877
|
+
if (!this.hasScope(principal, "conversations:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/read scope");
|
|
15878
|
+
const memory = this.getMemory();
|
|
15879
|
+
return json(res, 200, { enabled: Boolean(memory?.enabled), items: memory ? memory.list() : [] });
|
|
15880
|
+
}
|
|
15881
|
+
const memoryMatch = path.match(/^\/api\/v1\/memory\/([^/]+)$/);
|
|
15882
|
+
if (memoryMatch && method === "DELETE") {
|
|
15883
|
+
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
15884
|
+
const memory = this.getMemory();
|
|
15885
|
+
if (!memory || !memory.delete(memoryMatch[1])) return sendError(res, 404, "memory_not_found", "Memory item not found");
|
|
15886
|
+
return json(res, 200, { deleted: memoryMatch[1] });
|
|
15887
|
+
}
|
|
15888
|
+
if (path === `${API_PREFIX}/github/repositories` && (method === "GET" || method === "POST")) {
|
|
15889
|
+
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
15890
|
+
try {
|
|
15891
|
+
if (method === "GET") {
|
|
15892
|
+
return json(res, 200, { repositories: await this.github.listRepositories(url.searchParams.get("q") ?? "") });
|
|
15893
|
+
}
|
|
15894
|
+
const body = await readJsonBody(req);
|
|
15895
|
+
if (typeof body.name !== "string") return sendError(res, 400, "invalid_repository_name", "name is required");
|
|
15896
|
+
const repository = await this.github.createRepository({
|
|
15897
|
+
name: body.name.trim(),
|
|
15898
|
+
private: body.private !== false,
|
|
15899
|
+
...typeof body.description === "string" ? { description: body.description } : {}
|
|
15900
|
+
});
|
|
15901
|
+
return json(res, 201, { repository });
|
|
15902
|
+
} catch (error) {
|
|
15903
|
+
if (error instanceof AsterGitHubError) return sendError(res, error.status, error.code, error.message);
|
|
15904
|
+
throw error;
|
|
15905
|
+
}
|
|
14977
15906
|
}
|
|
14978
15907
|
const eventsMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/events$/);
|
|
14979
15908
|
if (eventsMatch && method === "GET") {
|
|
@@ -14990,7 +15919,7 @@ var init_aster_api = __esm({
|
|
|
14990
15919
|
const conversation = await this.authorizedConversation(store, principal, turnMatch[1]);
|
|
14991
15920
|
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
14992
15921
|
const body = await readJsonBody(req);
|
|
14993
|
-
if (Object.keys(body).some((key) => key !== "text" && key !== "clientTurnId")) return sendError(res, 400, "unsupported_field", "Turns accept text and an optional
|
|
15922
|
+
if (Object.keys(body).some((key) => key !== "text" && key !== "clientTurnId" && key !== "model")) return sendError(res, 400, "unsupported_field", "Turns accept text, an optional clientTurnId, and an optional model; the workspace is fixed by the conversation");
|
|
14994
15923
|
const hasClientTurnId = Object.hasOwn(body, "clientTurnId");
|
|
14995
15924
|
const clientTurnId = hasClientTurnId && typeof body.clientTurnId === "string" ? body.clientTurnId : void 0;
|
|
14996
15925
|
if (hasClientTurnId && (!clientTurnId || !CLIENT_TURN_ID_RE.test(clientTurnId))) return sendError(res, 400, "invalid_client_turn_id", "clientTurnId must be 1-128 ASCII letters, digits, '.', '_', ':', or '-' and start with a letter or digit");
|
|
@@ -14998,7 +15927,18 @@ var init_aster_api = __esm({
|
|
|
14998
15927
|
const text = requestText.trim();
|
|
14999
15928
|
if (!text || text.length > MAX_USER_TURN_CHARS) return sendError(res, 400, "invalid_turn", `Turn text must be 1-${MAX_USER_TURN_CHARS} characters`);
|
|
15000
15929
|
if (containsLikelyProviderSecret(text)) return sendError(res, 400, "secret_input_rejected", "Provider credentials must not be sent in conversation text");
|
|
15001
|
-
|
|
15930
|
+
let target = conversation;
|
|
15931
|
+
if (Object.hasOwn(body, "model")) {
|
|
15932
|
+
if (typeof body.model !== "string" || !await isSelectableModel(config2, body.model)) {
|
|
15933
|
+
return sendError(res, 400, "model_not_allowed", "Select a model from the model catalog");
|
|
15934
|
+
}
|
|
15935
|
+
if (body.model !== conversation.model) {
|
|
15936
|
+
const switched = await this.switchModel(res, store, conversation, body.model);
|
|
15937
|
+
if (!switched) return;
|
|
15938
|
+
target = switched;
|
|
15939
|
+
}
|
|
15940
|
+
}
|
|
15941
|
+
return await this.appendTurn(res, store, target, clientTurnId, requestText, text);
|
|
15002
15942
|
}
|
|
15003
15943
|
const cancelMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/cancel$/);
|
|
15004
15944
|
if (cancelMatch && method === "POST") {
|
|
@@ -15014,6 +15954,61 @@ var init_aster_api = __esm({
|
|
|
15014
15954
|
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
15015
15955
|
return lifecycleMatch[2] === "pause" ? this.pauseConversation(res, store, conversation) : this.resumeConversation(res, store, conversation);
|
|
15016
15956
|
}
|
|
15957
|
+
const artifactsMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/artifacts(\/file)?$/);
|
|
15958
|
+
if (artifactsMatch && method === "GET") {
|
|
15959
|
+
if (!this.hasScope(principal, "conversations:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/read scope");
|
|
15960
|
+
const conversation = await this.authorizedConversation(store, principal, artifactsMatch[1]);
|
|
15961
|
+
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
15962
|
+
const cell = this.readyCell(res, store, conversation.id);
|
|
15963
|
+
if (!cell) return;
|
|
15964
|
+
if (!this.runtime.listArtifacts || !this.runtime.readArtifact) return sendError(res, 503, "artifacts_unavailable", "Artifacts are not configured");
|
|
15965
|
+
if (!artifactsMatch[2]) return json(res, 200, { artifacts: await this.runtime.listArtifacts(cell) });
|
|
15966
|
+
try {
|
|
15967
|
+
const artifact = await this.runtime.readArtifact(cell, url.searchParams.get("path") ?? "");
|
|
15968
|
+
res.writeHead(200, {
|
|
15969
|
+
"content-type": artifact.mimeType,
|
|
15970
|
+
"content-length": artifact.data.length,
|
|
15971
|
+
"content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(artifact.name)}`,
|
|
15972
|
+
"cache-control": "no-store",
|
|
15973
|
+
"x-content-type-options": "nosniff"
|
|
15974
|
+
});
|
|
15975
|
+
res.end(artifact.data);
|
|
15976
|
+
return;
|
|
15977
|
+
} catch (error) {
|
|
15978
|
+
const code = error instanceof Error ? error.message : "";
|
|
15979
|
+
if (code === "invalid_artifact_path") return sendError(res, 400, code, "Artifact path is invalid");
|
|
15980
|
+
if (code === "artifact_too_large") return sendError(res, 413, code, "Artifact is larger than 50 MB");
|
|
15981
|
+
return sendError(res, 404, "artifact_not_found", "Artifact not found");
|
|
15982
|
+
}
|
|
15983
|
+
}
|
|
15984
|
+
const checkpointsMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/checkpoints$/);
|
|
15985
|
+
if (checkpointsMatch && method === "GET") {
|
|
15986
|
+
if (!this.hasScope(principal, "conversations:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/read scope");
|
|
15987
|
+
const conversation = await this.authorizedConversation(store, principal, checkpointsMatch[1]);
|
|
15988
|
+
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
15989
|
+
const cell = this.readyCell(res, store, conversation.id);
|
|
15990
|
+
if (!cell) return;
|
|
15991
|
+
if (!this.runtime.listCheckpoints) return sendError(res, 503, "checkpoints_unavailable", "Checkpoints are not configured");
|
|
15992
|
+
return json(res, 200, { checkpoints: await this.runtime.listCheckpoints(cell) });
|
|
15993
|
+
}
|
|
15994
|
+
const restoreMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/checkpoints\/([0-9a-f]{7,40})\/restore$/);
|
|
15995
|
+
if (restoreMatch && method === "POST") {
|
|
15996
|
+
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
15997
|
+
const conversation = await this.authorizedConversation(store, principal, restoreMatch[1]);
|
|
15998
|
+
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
15999
|
+
if (conversation.activeRunId) return sendError(res, 409, "turn_active", "Wait for the current turn to finish or cancel it before restoring");
|
|
16000
|
+
const cell = this.readyCell(res, store, conversation.id);
|
|
16001
|
+
if (!cell) return;
|
|
16002
|
+
if (!this.runtime.restoreCheckpoint) return sendError(res, 503, "checkpoints_unavailable", "Checkpoints are not configured");
|
|
16003
|
+
try {
|
|
16004
|
+
const checkpoint = await this.runtime.restoreCheckpoint(cell, conversation.id, restoreMatch[2]);
|
|
16005
|
+
store.appendEvent(conversation.id, null, "checkpoint.restored", { restoredFrom: restoreMatch[2], ...checkpoint });
|
|
16006
|
+
return json(res, 200, { checkpoint });
|
|
16007
|
+
} catch (error) {
|
|
16008
|
+
if (error instanceof Error && error.message === "checkpoint_not_found") return sendError(res, 404, "checkpoint_not_found", "Checkpoint not found");
|
|
16009
|
+
return sendError(res, 500, "checkpoint_restore_failed", "The workspace could not be restored");
|
|
16010
|
+
}
|
|
16011
|
+
}
|
|
15017
16012
|
const conversationMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)$/);
|
|
15018
16013
|
if (conversationMatch && method === "DELETE") {
|
|
15019
16014
|
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
@@ -15025,7 +16020,7 @@ var init_aster_api = __esm({
|
|
|
15025
16020
|
if (!this.hasScope(principal, "conversations:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/read scope");
|
|
15026
16021
|
const conversation = await this.authorizedConversation(store, principal, conversationMatch[1]);
|
|
15027
16022
|
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
15028
|
-
return json(res, 200, publicConversation(conversation));
|
|
16023
|
+
return json(res, 200, publicConversation(conversation, store.getConversationProject(conversation.id)));
|
|
15029
16024
|
}
|
|
15030
16025
|
const approvalMatch = path.match(/^\/api\/v1\/approvals\/([^/]+)$/);
|
|
15031
16026
|
if (approvalMatch && method === "GET") {
|
|
@@ -15064,6 +16059,12 @@ var init_aster_api = __esm({
|
|
|
15064
16059
|
}
|
|
15065
16060
|
this.store?.close();
|
|
15066
16061
|
this.store = void 0;
|
|
16062
|
+
this.memory?.close();
|
|
16063
|
+
this.memory = void 0;
|
|
16064
|
+
this.githubProxy?.close();
|
|
16065
|
+
this.githubProxy = void 0;
|
|
16066
|
+
this.push?.close();
|
|
16067
|
+
this.push = void 0;
|
|
15067
16068
|
}
|
|
15068
16069
|
getStore() {
|
|
15069
16070
|
return this.store ??= new AsterStore();
|
|
@@ -15115,6 +16116,14 @@ var init_aster_api = __esm({
|
|
|
15115
16116
|
} else if (run.status === "needs_input") {
|
|
15116
16117
|
runs.transition(run.id, "cancelled", "approval_expired_on_restart");
|
|
15117
16118
|
store.updateConversationStatus(conversation.id, run.id, "interrupted", { reason: "approval_expired_on_restart" });
|
|
16119
|
+
} else {
|
|
16120
|
+
const mapping = store.getCellMapping(conversation.id);
|
|
16121
|
+
if (mapping?.cellId && this.runtime.resumeTurn) {
|
|
16122
|
+
this.runtime.resumeTurn(this.cellTurn(store, conversation, run.id, mapping.cellId, mapping.eventCursor, run.task ?? ""));
|
|
16123
|
+
} else {
|
|
16124
|
+
runs.transition(run.id, "failed", "server_restarted");
|
|
16125
|
+
store.updateConversationStatus(conversation.id, run.id, "interrupted", { reason: "server_restart_recovery" });
|
|
16126
|
+
}
|
|
15118
16127
|
}
|
|
15119
16128
|
}
|
|
15120
16129
|
} finally {
|
|
@@ -15123,9 +16132,44 @@ var init_aster_api = __esm({
|
|
|
15123
16132
|
})();
|
|
15124
16133
|
return this.recovery;
|
|
15125
16134
|
}
|
|
15126
|
-
|
|
15127
|
-
|
|
15128
|
-
const
|
|
16135
|
+
/** A turn handle for re-attaching to a cell turn after a restart (no prompt is sent). */
|
|
16136
|
+
cellTurn(store, conversation, runId, cellId, cursor, userText) {
|
|
16137
|
+
const publishEvent = (type, data) => {
|
|
16138
|
+
store.appendEvent(conversation.id, runId, type, data);
|
|
16139
|
+
};
|
|
16140
|
+
return {
|
|
16141
|
+
runId,
|
|
16142
|
+
conversation,
|
|
16143
|
+
cellId,
|
|
16144
|
+
cellEventCursor: cursor,
|
|
16145
|
+
userText,
|
|
16146
|
+
sessionFile: {},
|
|
16147
|
+
messages: [],
|
|
16148
|
+
executor: new ToolExecutor([]),
|
|
16149
|
+
allowedTools: /* @__PURE__ */ new Set(),
|
|
16150
|
+
maxToolIterations: null,
|
|
16151
|
+
maxRuntimeMs: null,
|
|
16152
|
+
askPermission: async () => "deny",
|
|
16153
|
+
publishEvent,
|
|
16154
|
+
onCellCursor: (next) => store.advanceCellEventCursor(conversation.id, next),
|
|
16155
|
+
finish: (status, reason) => {
|
|
16156
|
+
const runs = new RunStore();
|
|
16157
|
+
try {
|
|
16158
|
+
const run = runs.getRun(runId);
|
|
16159
|
+
if (run && ["queued", "running", "waiting", "needs_input"].includes(run.status)) runs.transition(runId, status, reason ?? status);
|
|
16160
|
+
} catch {
|
|
16161
|
+
} finally {
|
|
16162
|
+
runs.close();
|
|
16163
|
+
}
|
|
16164
|
+
const updated = store.updateConversationStatus(conversation.id, runId, status, reason ? { reason } : {});
|
|
16165
|
+
if (updated) store.appendEvent(conversation.id, runId, status, reason ? { reason } : {});
|
|
16166
|
+
if (updated) this.notifyTurnEnd(store, conversation.id, runId, status, reason, userText, 0);
|
|
16167
|
+
}
|
|
16168
|
+
};
|
|
16169
|
+
}
|
|
16170
|
+
async createConversation(res, store, principal, workspace, model, idempotencyKey, kind, repository) {
|
|
16171
|
+
const candidateId = randomUUID7();
|
|
16172
|
+
const candidateSessionId = randomUUID7();
|
|
15129
16173
|
const reservation = store.reserveCellConversation({
|
|
15130
16174
|
conversationId: candidateId,
|
|
15131
16175
|
credentialId: principal.credentialId,
|
|
@@ -15140,12 +16184,47 @@ var init_aster_api = __esm({
|
|
|
15140
16184
|
if (mapping.status === "destroyed") return sendError(res, 410, "conversation_destroyed", "Conversation was already destroyed");
|
|
15141
16185
|
if (mapping.status === "ready") {
|
|
15142
16186
|
const existing = store.getConversation(mapping.conversationId);
|
|
15143
|
-
if (existing) return json(res, 200, publicConversation(existing));
|
|
16187
|
+
if (existing) return json(res, 200, publicConversation(existing, store.getConversationProject(existing.id)));
|
|
15144
16188
|
}
|
|
15145
16189
|
if (!this.runtime.provisionConversation) {
|
|
15146
16190
|
store.setCellStatus(mapping.conversationId, "failed");
|
|
15147
16191
|
return sendError(res, 503, "hotcell_unavailable", "Hotcell conversation runtime is not configured");
|
|
15148
16192
|
}
|
|
16193
|
+
let project;
|
|
16194
|
+
if (kind) {
|
|
16195
|
+
let stored = store.getConversationProject(mapping.conversationId);
|
|
16196
|
+
if (!stored) {
|
|
16197
|
+
if (kind === "code") {
|
|
16198
|
+
let repo;
|
|
16199
|
+
try {
|
|
16200
|
+
repo = await this.github.getRepository(repository);
|
|
16201
|
+
} catch (error) {
|
|
16202
|
+
if (error instanceof AsterGitHubError) return sendError(res, error.status, error.code, error.message);
|
|
16203
|
+
throw error;
|
|
16204
|
+
}
|
|
16205
|
+
store.setConversationProject(mapping.conversationId, {
|
|
16206
|
+
kind,
|
|
16207
|
+
repository: repo.fullName,
|
|
16208
|
+
branch: `aster/${mapping.conversationId.slice(0, 8)}`
|
|
16209
|
+
});
|
|
16210
|
+
stored = { kind, repository: repo.fullName, branch: `aster/${mapping.conversationId.slice(0, 8)}` };
|
|
16211
|
+
project = { kind, repository: repo.fullName, baseBranch: repo.defaultBranch, branch: stored.branch };
|
|
16212
|
+
} else {
|
|
16213
|
+
store.setConversationProject(mapping.conversationId, { kind, repository: null, branch: null });
|
|
16214
|
+
stored = { kind, repository: null, branch: null };
|
|
16215
|
+
}
|
|
16216
|
+
}
|
|
16217
|
+
if (!project) {
|
|
16218
|
+
project = stored.kind === "code" ? { kind: "code", repository: stored.repository, baseBranch: (await this.github.getRepository(stored.repository).catch(() => void 0))?.defaultBranch ?? "main", branch: stored.branch } : { kind: "chat" };
|
|
16219
|
+
}
|
|
16220
|
+
if (project.kind === "code" && project.repository && this.githubProxyUrl) {
|
|
16221
|
+
project.github = {
|
|
16222
|
+
token: store.getOrCreateGitHubGrant(mapping.conversationId, project.repository),
|
|
16223
|
+
gitUrl: `${this.githubProxyUrl}/git`,
|
|
16224
|
+
apiUrl: `${this.githubProxyUrl}/api`
|
|
16225
|
+
};
|
|
16226
|
+
}
|
|
16227
|
+
}
|
|
15149
16228
|
try {
|
|
15150
16229
|
const allowCreate = !mapping.createAttempted;
|
|
15151
16230
|
if (allowCreate) store.markCellCreateAttempted(mapping.conversationId);
|
|
@@ -15157,7 +16236,8 @@ var init_aster_api = __esm({
|
|
|
15157
16236
|
workspaceRoot: workspace.rootPath,
|
|
15158
16237
|
model,
|
|
15159
16238
|
allowCreate,
|
|
15160
|
-
onCellCreated: (cellId) => store.setCellId(mapping.conversationId, cellId)
|
|
16239
|
+
onCellCreated: (cellId) => store.setCellId(mapping.conversationId, cellId),
|
|
16240
|
+
...project ? { project } : {}
|
|
15161
16241
|
});
|
|
15162
16242
|
store.setCellId(mapping.conversationId, provisioned.cellId);
|
|
15163
16243
|
const conversation = store.getConversation(mapping.conversationId) ?? store.createConversation({
|
|
@@ -15181,7 +16261,7 @@ var init_aster_api = __esm({
|
|
|
15181
16261
|
await saveSession(sessionFile);
|
|
15182
16262
|
store.setCellStatus(mapping.conversationId, "ready");
|
|
15183
16263
|
store.appendEvent(mapping.conversationId, null, "conversation.created", { conversationId: mapping.conversationId, workspaceId: workspace.id, model });
|
|
15184
|
-
json(res, reservation.kind === "created" ? 201 : 200, publicConversation(conversation));
|
|
16264
|
+
json(res, reservation.kind === "created" ? 201 : 200, publicConversation(conversation, store.getConversationProject(conversation.id)));
|
|
15185
16265
|
} catch {
|
|
15186
16266
|
const current = store.getCellMapping(mapping.conversationId);
|
|
15187
16267
|
if (current?.cellId && this.runtime.destroyCell) {
|
|
@@ -15219,7 +16299,7 @@ var init_aster_api = __esm({
|
|
|
15219
16299
|
sendError(res, 409, "conversation_cell_unavailable", "Conversation cell is not available");
|
|
15220
16300
|
return;
|
|
15221
16301
|
}
|
|
15222
|
-
const runId =
|
|
16302
|
+
const runId = randomUUID7();
|
|
15223
16303
|
if (!idempotent && !store.beginTurn(conversation.id, runId)) {
|
|
15224
16304
|
sendError(res, 409, "conversation_busy", "A turn is already active for this conversation");
|
|
15225
16305
|
return;
|
|
@@ -15296,6 +16376,7 @@ var init_aster_api = __esm({
|
|
|
15296
16376
|
const publishEvent = (type, data) => {
|
|
15297
16377
|
store.appendEvent(conversation.id, runId, type, data);
|
|
15298
16378
|
};
|
|
16379
|
+
const turnStartSequence = store.getConversation(conversation.id)?.lastEventSequence ?? 0;
|
|
15299
16380
|
const active = {
|
|
15300
16381
|
runId,
|
|
15301
16382
|
conversation,
|
|
@@ -15314,8 +16395,20 @@ var init_aster_api = __esm({
|
|
|
15314
16395
|
finish: (status, reason) => {
|
|
15315
16396
|
const updated = store.updateConversationStatus(conversation.id, runId, status, reason ? { reason } : {});
|
|
15316
16397
|
if (updated) store.appendEvent(conversation.id, runId, status, reason ? { reason } : {});
|
|
16398
|
+
if (updated) this.notifyTurnEnd(store, conversation.id, runId, status, reason, text, turnStartSequence);
|
|
15317
16399
|
}
|
|
15318
16400
|
};
|
|
16401
|
+
const memory = this.getMemory();
|
|
16402
|
+
if (memory) {
|
|
16403
|
+
active.memory = { note: memory.brief(conversation.id, text), snapshot: memory.snapshot() };
|
|
16404
|
+
const firstSequence = store.getConversation(conversation.id)?.lastEventSequence ?? 0;
|
|
16405
|
+
active.beforeFinish = async () => {
|
|
16406
|
+
const reply = collectAssistantText(store, conversation.id, runId, firstSequence);
|
|
16407
|
+
const project = store.getConversationProject(conversation.id);
|
|
16408
|
+
const changes = await memory.remember({ conversationId: conversation.id, project, userText: text, assistantText: reply });
|
|
16409
|
+
if (changes.length) publishEvent("memory.updated", { items: changes });
|
|
16410
|
+
};
|
|
16411
|
+
}
|
|
15319
16412
|
this.runtime.startTurn(active);
|
|
15320
16413
|
json(res, 202, acceptedResponse);
|
|
15321
16414
|
} catch {
|
|
@@ -15466,6 +16559,7 @@ var init_aster_api = __esm({
|
|
|
15466
16559
|
if (mapping.status === "destroyed") return json(res, 200, { conversationId: conversation.id, status: "destroyed" });
|
|
15467
16560
|
if (!this.runtime.destroyCell) return sendError(res, 503, "hotcell_destroy_unavailable", "Hotcell cleanup is not configured");
|
|
15468
16561
|
store.setCellStatus(conversation.id, "destroying");
|
|
16562
|
+
store.revokeGitHubGrant(conversation.id);
|
|
15469
16563
|
if (conversation.activeRunId) {
|
|
15470
16564
|
for (const approval of store.cancelPendingApprovals(conversation.id)) {
|
|
15471
16565
|
const waiter = this.waiters.get(approval.id);
|
|
@@ -15491,6 +16585,50 @@ var init_aster_api = __esm({
|
|
|
15491
16585
|
sendError(res, 503, "cleanup_pending", "Cell cleanup is pending; retry DELETE to revoke credentials and remove the cell");
|
|
15492
16586
|
}
|
|
15493
16587
|
}
|
|
16588
|
+
/** The conversation's cell id when it can take control requests; otherwise sends the error. */
|
|
16589
|
+
readyCell(res, store, conversationId) {
|
|
16590
|
+
const mapping = store.getCellMapping(conversationId);
|
|
16591
|
+
if (mapping?.status === "paused") {
|
|
16592
|
+
sendError(res, 409, "conversation_paused", "Conversation is paused; resume it first");
|
|
16593
|
+
return void 0;
|
|
16594
|
+
}
|
|
16595
|
+
if (!mapping?.cellId || mapping.status !== "ready") {
|
|
16596
|
+
sendError(res, 409, "conversation_cell_unavailable", "Conversation cell is not available");
|
|
16597
|
+
return void 0;
|
|
16598
|
+
}
|
|
16599
|
+
return mapping.cellId;
|
|
16600
|
+
}
|
|
16601
|
+
/** Switch the cell session first, then the record, so a failed switch leaves both on the old model. */
|
|
16602
|
+
async switchModel(res, store, conversation, model) {
|
|
16603
|
+
if (conversation.activeRunId) {
|
|
16604
|
+
sendError(res, 409, "conversation_busy", "A turn is already active for this conversation");
|
|
16605
|
+
return void 0;
|
|
16606
|
+
}
|
|
16607
|
+
const cell = this.readyCell(res, store, conversation.id);
|
|
16608
|
+
if (!cell) return void 0;
|
|
16609
|
+
if (!this.runtime.setModel) {
|
|
16610
|
+
sendError(res, 503, "model_switch_unavailable", "Model switching is not configured");
|
|
16611
|
+
return void 0;
|
|
16612
|
+
}
|
|
16613
|
+
try {
|
|
16614
|
+
await this.runtime.setModel(cell, model);
|
|
16615
|
+
} catch {
|
|
16616
|
+
sendError(res, 503, "model_switch_failed", "The conversation's model could not be switched");
|
|
16617
|
+
return void 0;
|
|
16618
|
+
}
|
|
16619
|
+
if (!store.setConversationModel(conversation.id, model)) {
|
|
16620
|
+
sendError(res, 409, "conversation_busy", "A turn is already active for this conversation");
|
|
16621
|
+
return void 0;
|
|
16622
|
+
}
|
|
16623
|
+
try {
|
|
16624
|
+
const sessionPath = resolve8(sessionsDir(), conversation.sessionId + ".json");
|
|
16625
|
+
const sessionFile = await loadSession(sessionPath);
|
|
16626
|
+
sessionFile.model = model;
|
|
16627
|
+
await saveSession(sessionFile);
|
|
16628
|
+
} catch {
|
|
16629
|
+
}
|
|
16630
|
+
return store.getConversation(conversation.id);
|
|
16631
|
+
}
|
|
15494
16632
|
async cancelConversation(res, store, conversation) {
|
|
15495
16633
|
const runId = conversation.activeRunId;
|
|
15496
16634
|
if (!runId) {
|
|
@@ -15611,7 +16749,31 @@ data: ${JSON.stringify(event.data)}
|
|
|
15611
16749
|
// src/server/aster-cell-runtime.ts
|
|
15612
16750
|
import { execFile as execFile5 } from "child_process";
|
|
15613
16751
|
import { promisify as promisify4 } from "util";
|
|
15614
|
-
import { randomUUID as
|
|
16752
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
16753
|
+
async function writeCellFileChunked(cell, path, content) {
|
|
16754
|
+
if (Buffer.byteLength(content, "utf8") <= CELL_FILE_WRITE_CHUNK_CHARS) {
|
|
16755
|
+
await cell.writeFile(path, content, { mode: "0600" });
|
|
16756
|
+
return;
|
|
16757
|
+
}
|
|
16758
|
+
const partsDir = `${path}.parts`;
|
|
16759
|
+
const encoded = Buffer.from(content, "utf8").toString("base64");
|
|
16760
|
+
const partPaths = [];
|
|
16761
|
+
await cell.mkdir(partsDir, { parents: true });
|
|
16762
|
+
try {
|
|
16763
|
+
for (let offset = 0, index = 0; offset < encoded.length; offset += CELL_FILE_WRITE_CHUNK_CHARS, index++) {
|
|
16764
|
+
const partPath = `${partsDir}/${String(index).padStart(6, "0")}.b64`;
|
|
16765
|
+
await cell.writeFile(partPath, encoded.slice(offset, offset + CELL_FILE_WRITE_CHUNK_CHARS), { mode: "0600" });
|
|
16766
|
+
partPaths.push(partPath);
|
|
16767
|
+
}
|
|
16768
|
+
const result2 = await cell.exec(
|
|
16769
|
+
`cat ${partPaths.map(shellQuote2).join(" ")} | base64 -d > ${shellQuote2(path)} && chmod 600 ${shellQuote2(path)}`
|
|
16770
|
+
);
|
|
16771
|
+
if (result2.exitCode !== 0) throw new Error("cell_control_failed");
|
|
16772
|
+
} finally {
|
|
16773
|
+
await cell.exec(`rm -rf ${shellQuote2(partsDir)}`).catch(() => {
|
|
16774
|
+
});
|
|
16775
|
+
}
|
|
16776
|
+
}
|
|
15615
16777
|
async function defaultArchiveWorkspace(workspaceRoot) {
|
|
15616
16778
|
const { stdout } = await execFileAsync3("git", ["-C", workspaceRoot, "archive", "--format=tar", "HEAD"], {
|
|
15617
16779
|
encoding: "buffer",
|
|
@@ -15619,17 +16781,73 @@ async function defaultArchiveWorkspace(workspaceRoot) {
|
|
|
15619
16781
|
});
|
|
15620
16782
|
return Buffer.from(stdout).toString("base64");
|
|
15621
16783
|
}
|
|
16784
|
+
function projectNote(project) {
|
|
16785
|
+
const artifacts = "Save anything meant for the user to look at (documents, slides, PDFs, spreadsheets, web pages, images, reports) in /workspace/artifacts/. Files there appear in the user's Aster app (an iPhone), where they can preview, download, and share them. Unless the user asks for an editable format, deliver visual documents (slides, reports, one-pagers) as PDF: write HTML/CSS (one page per slide, e.g. @page { size: 1280px 720px; margin: 0 }) and print it with `chromium --headless --no-sandbox --disable-gpu --no-pdf-header-footer --print-to-pdf=/workspace/artifacts/NAME.pdf FILE.html`. For editable files use python-pptx, python-docx, or openpyxl; matplotlib is available for charts. Keep drafts and source files outside artifacts/ so only finished deliverables appear there.";
|
|
16786
|
+
if (project.kind === "chat") {
|
|
16787
|
+
return `[Aster chat. /workspace is this conversation's private Linux sandbox with internet access. ${artifacts}]`;
|
|
16788
|
+
}
|
|
16789
|
+
const { repository, branch, baseBranch } = project;
|
|
16790
|
+
return `[Aster code chat. GitHub repository ${repository} is cloned at /workspace on branch ${branch} (from ${baseBranch}). Commit your work and push with \`git push -u origin HEAD\`; credentials are preconfigured, so never print or write tokens. Only this repository is reachable from here. To open a pull request: curl -s -X POST "\${ASTER_GITHUB_API_URL:-$GITHUB_BASE_URL}/repos/${repository}/pulls" -H "Authorization: Bearer \${ASTER_GITHUB_TOKEN:-$GITHUB_API_KEY}" -H "Accept: application/vnd.github+json" -d '{"title":"\u2026","head":"${branch}","base":"${baseBranch}","body":"\u2026"}'. ${artifacts} The artifacts folder is not committed to the repository.]`;
|
|
16791
|
+
}
|
|
16792
|
+
function isSafeArtifactPath(path) {
|
|
16793
|
+
if (!path || path.length > 500 || path.startsWith("/") || /[\x00-\x1f\x7f]/.test(path)) return false;
|
|
16794
|
+
return path.split("/").every((part) => part !== "" && part !== "." && part !== ".." && !part.startsWith("."));
|
|
16795
|
+
}
|
|
16796
|
+
function shellQuote2(value) {
|
|
16797
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
16798
|
+
}
|
|
16799
|
+
function artifactMimeType(path) {
|
|
16800
|
+
const extension = path.split(".").pop()?.toLowerCase() ?? "";
|
|
16801
|
+
return ARTIFACT_MIME_TYPES[extension] ?? "application/octet-stream";
|
|
16802
|
+
}
|
|
16803
|
+
function checkpointMessage(userText) {
|
|
16804
|
+
const firstLine2 = redactLikelySecrets(userText).split("\n").find((line) => line.trim())?.trim() ?? "Turn";
|
|
16805
|
+
const subject = firstLine2.length > 72 ? firstLine2.slice(0, 71) + "\u2026" : firstLine2;
|
|
16806
|
+
return `${subject}
|
|
16807
|
+
`;
|
|
16808
|
+
}
|
|
16809
|
+
function summarizeToolInput(toolName, args) {
|
|
16810
|
+
const record = args && typeof args === "object" ? args : {};
|
|
16811
|
+
const preferred = toolName === "bash" ? record.command : record.path ?? record.pattern ?? record.url ?? record.query;
|
|
16812
|
+
const text = typeof preferred === "string" ? preferred : JSON.stringify(args ?? {});
|
|
16813
|
+
return boundedToolText(text, MAX_TOOL_INPUT_CHARS);
|
|
16814
|
+
}
|
|
16815
|
+
function boundedToolText(text, limit) {
|
|
16816
|
+
const redacted = redactLikelySecrets(text);
|
|
16817
|
+
if (redacted.length <= limit) return redacted;
|
|
16818
|
+
const half = Math.floor((limit - 40) / 2);
|
|
16819
|
+
return `${redacted.slice(0, half)}
|
|
16820
|
+
\u2026 [${redacted.length - 2 * half} characters omitted] \u2026
|
|
16821
|
+
${redacted.slice(-half)}`;
|
|
16822
|
+
}
|
|
15622
16823
|
function sleep4(ms) {
|
|
15623
16824
|
return new Promise((resolve15) => setTimeout(resolve15, ms));
|
|
15624
16825
|
}
|
|
15625
|
-
var execFileAsync3, CELL_WORKSPACE, CELL_STATE_DIR, CELL_CONTROL_DIR, BRIDGE_MARKER, CONTROL_RUNNER, BRIDGE_RUNNER, HEALTH_TIMEOUT_MS, EVENT_POLL_MS, AsterHotcellRuntime;
|
|
16826
|
+
var execFileAsync3, CELL_WORKSPACE, CELL_STATE_DIR, CHECKPOINT_MESSAGE_FILE, INSTALLED_RUNTIME_PACKAGE, NOTES_FILE, MEMORY_FILE, MEMORY_EXTRACTION_TIMEOUT_MS, LIVENESS_CHECK_MS, ARTIFACTS_DIR, MAX_ARTIFACTS, MAX_ARTIFACT_BYTES, BRANCH_RE, REPOSITORY_RE, GIT, COMMIT_SHA_RE, MAX_TOOL_OUTPUT_CHARS, MAX_TOOL_INPUT_CHARS, MAX_CHECKPOINTS, CELL_CONTROL_DIR, BRIDGE_MARKER, CONTROL_RUNNER, BRIDGE_RUNNER, HEALTH_TIMEOUT_MS, EVENT_POLL_MS, AsterHotcellRuntime, CELL_FILE_WRITE_CHUNK_CHARS, ARTIFACT_MIME_TYPES;
|
|
15626
16827
|
var init_aster_cell_runtime = __esm({
|
|
15627
16828
|
"src/server/aster-cell-runtime.ts"() {
|
|
15628
16829
|
"use strict";
|
|
15629
16830
|
init_version();
|
|
16831
|
+
init_aster_tools();
|
|
15630
16832
|
execFileAsync3 = promisify4(execFile5);
|
|
15631
16833
|
CELL_WORKSPACE = "/workspace";
|
|
15632
16834
|
CELL_STATE_DIR = "/workspace/.aster";
|
|
16835
|
+
CHECKPOINT_MESSAGE_FILE = `${CELL_STATE_DIR}/checkpoint-message`;
|
|
16836
|
+
INSTALLED_RUNTIME_PACKAGE = "/opt/autopilot/node_modules/autopilot-ai/package.json";
|
|
16837
|
+
NOTES_FILE = `${CELL_STATE_DIR}/pending-notes`;
|
|
16838
|
+
MEMORY_FILE = `${CELL_STATE_DIR}/memory.md`;
|
|
16839
|
+
MEMORY_EXTRACTION_TIMEOUT_MS = 15e3;
|
|
16840
|
+
LIVENESS_CHECK_MS = 2e4;
|
|
16841
|
+
ARTIFACTS_DIR = `${CELL_WORKSPACE}/artifacts`;
|
|
16842
|
+
MAX_ARTIFACTS = 500;
|
|
16843
|
+
MAX_ARTIFACT_BYTES = 50 * 1024 * 1024;
|
|
16844
|
+
BRANCH_RE = /^[A-Za-z0-9._/-]{1,200}$/;
|
|
16845
|
+
REPOSITORY_RE = /^[A-Za-z0-9-]{1,39}\/[A-Za-z0-9._-]{1,100}$/;
|
|
16846
|
+
GIT = `git -C ${CELL_WORKSPACE} -c user.name=Autopilot -c user.email=autopilot@aster.invalid -c commit.gpgsign=false`;
|
|
16847
|
+
COMMIT_SHA_RE = /^[0-9a-f]{7,40}$/;
|
|
16848
|
+
MAX_TOOL_OUTPUT_CHARS = 16e3;
|
|
16849
|
+
MAX_TOOL_INPUT_CHARS = 2e3;
|
|
16850
|
+
MAX_CHECKPOINTS = 100;
|
|
15633
16851
|
CELL_CONTROL_DIR = `${CELL_STATE_DIR}/control`;
|
|
15634
16852
|
BRIDGE_MARKER = "startAsterCellBridge";
|
|
15635
16853
|
CONTROL_RUNNER = `node --input-type=module -e 'import { executeAsterCellControl } from "/opt/autopilot/node_modules/autopilot-ai/dist/sdk/index.js";const r = await executeAsterCellControl(process.argv[1]);process.stdout.write(JSON.stringify(r) + "\\n");'`;
|
|
@@ -15643,6 +16861,14 @@ var init_aster_cell_runtime = __esm({
|
|
|
15643
16861
|
this.archiveWorkspace = options.archiveWorkspace ?? defaultArchiveWorkspace;
|
|
15644
16862
|
this.pollIntervalMs = options.pollIntervalMs ?? EVENT_POLL_MS;
|
|
15645
16863
|
this.healthTimeoutMs = options.healthTimeoutMs ?? HEALTH_TIMEOUT_MS;
|
|
16864
|
+
this.idlePauseMs = options.idlePauseMs ?? 0;
|
|
16865
|
+
if (this.idlePauseMs > 0) {
|
|
16866
|
+
const timer3 = setInterval(() => {
|
|
16867
|
+
void this.pauseIdleCells(this.idlePauseMs).catch(() => {
|
|
16868
|
+
});
|
|
16869
|
+
}, options.idleSweepIntervalMs ?? 6e4);
|
|
16870
|
+
timer3.unref();
|
|
16871
|
+
}
|
|
15646
16872
|
}
|
|
15647
16873
|
provider;
|
|
15648
16874
|
packageSpec;
|
|
@@ -15650,22 +16876,63 @@ var init_aster_cell_runtime = __esm({
|
|
|
15650
16876
|
pollIntervalMs;
|
|
15651
16877
|
healthTimeoutMs;
|
|
15652
16878
|
activeRuns = /* @__PURE__ */ new Map();
|
|
16879
|
+
idlePauseMs;
|
|
16880
|
+
/**
|
|
16881
|
+
* Pause running cells that have no active turn and no API activity within `idleMs`,
|
|
16882
|
+
* least recently used first, stopping after `limit` pauses. Returns the paused ids.
|
|
16883
|
+
*/
|
|
16884
|
+
async pauseIdleCells(idleMs, limit = Infinity) {
|
|
16885
|
+
const busy = new Set([...this.activeRuns.values()].map((run) => run.cellId));
|
|
16886
|
+
const now2 = Date.now();
|
|
16887
|
+
const candidates = (await this.provider.listConversationCells()).filter((cell) => cell.status === "running" && !busy.has(cell.id) && now2 - Date.parse(cell.lastActivityAt) >= idleMs).sort((a, b) => Date.parse(a.lastActivityAt) - Date.parse(b.lastActivityAt));
|
|
16888
|
+
const paused = [];
|
|
16889
|
+
for (const cell of candidates) {
|
|
16890
|
+
if (paused.length >= limit) break;
|
|
16891
|
+
try {
|
|
16892
|
+
await this.provider.pauseConversationCell(cell.id);
|
|
16893
|
+
paused.push(cell.id);
|
|
16894
|
+
} catch {
|
|
16895
|
+
}
|
|
16896
|
+
}
|
|
16897
|
+
return paused;
|
|
16898
|
+
}
|
|
15653
16899
|
async provisionConversation(input) {
|
|
15654
16900
|
let cell = await this.provider.findConversationCell(input.conversationId);
|
|
15655
16901
|
if (!cell) {
|
|
15656
16902
|
if (!input.allowCreate) throw new Error("hotcell_create_uncertain");
|
|
15657
|
-
|
|
16903
|
+
const create = () => this.provider.createConversationCell({
|
|
15658
16904
|
conversationId: input.conversationId,
|
|
15659
16905
|
workspaceId: input.workspaceId,
|
|
15660
|
-
model: input.model
|
|
16906
|
+
model: input.model,
|
|
16907
|
+
...input.project ? { kind: input.project.kind } : {},
|
|
16908
|
+
...input.project?.github ? {
|
|
16909
|
+
githubViaProxy: true,
|
|
16910
|
+
env: {
|
|
16911
|
+
ASTER_GITHUB_TOKEN: input.project.github.token,
|
|
16912
|
+
ASTER_GITHUB_GIT_URL: input.project.github.gitUrl,
|
|
16913
|
+
ASTER_GITHUB_API_URL: input.project.github.apiUrl
|
|
16914
|
+
}
|
|
16915
|
+
} : {}
|
|
15661
16916
|
});
|
|
16917
|
+
try {
|
|
16918
|
+
cell = await create();
|
|
16919
|
+
} catch (error) {
|
|
16920
|
+
if ((await this.pauseIdleCells(6e4, 1).catch(() => [])).length === 0) throw error;
|
|
16921
|
+
cell = await create();
|
|
16922
|
+
}
|
|
15662
16923
|
}
|
|
15663
16924
|
input.onCellCreated(cell.getInfo().id);
|
|
15664
|
-
|
|
16925
|
+
if (input.project?.kind === "code") {
|
|
16926
|
+
await this.cloneRepository(cell, input.project);
|
|
16927
|
+
} else if (input.project?.kind !== "chat") {
|
|
16928
|
+
await this.seedWorkspace(cell, input.workspaceRoot);
|
|
16929
|
+
}
|
|
16930
|
+
await this.ensureRepository(cell, input.project?.kind === "code");
|
|
16931
|
+
if (input.project) await this.addNote(cell, projectNote(input.project));
|
|
15665
16932
|
await this.installRuntime(cell);
|
|
15666
16933
|
await this.ensureBridge(cell);
|
|
15667
16934
|
await this.control(cell, "POST", "/rpc", {
|
|
15668
|
-
id:
|
|
16935
|
+
id: randomUUID8(),
|
|
15669
16936
|
type: "new_session",
|
|
15670
16937
|
sessionId: input.sessionId,
|
|
15671
16938
|
config: { model: input.model, cwd: CELL_WORKSPACE }
|
|
@@ -15681,19 +16948,76 @@ var init_aster_cell_runtime = __esm({
|
|
|
15681
16948
|
async resumeCell(cellId) {
|
|
15682
16949
|
await this.provider.resumeConversationCell(cellId);
|
|
15683
16950
|
}
|
|
16951
|
+
/** Switch the cell session's model between turns. The bridge persists it for session restore. */
|
|
16952
|
+
async setModel(cellId, model) {
|
|
16953
|
+
await this.ensureReady(cellId);
|
|
16954
|
+
await this.controlById(cellId, "POST", "/rpc", { id: randomUUID8(), type: "set_model", modelId: model });
|
|
16955
|
+
}
|
|
16956
|
+
/** Reinstall the runtime if the cell's image lost it and restart the bridge if it isn't running. */
|
|
16957
|
+
async ensureReady(cellId) {
|
|
16958
|
+
const cell = await this.provider.getCell(cellId);
|
|
16959
|
+
await this.installRuntime(cell);
|
|
16960
|
+
await this.ensureBridge(cell);
|
|
16961
|
+
}
|
|
16962
|
+
/** Newest-first git history of the cell workspace; one commit per completed turn. */
|
|
16963
|
+
async listCheckpoints(cellId) {
|
|
16964
|
+
const cell = await this.provider.getCell(cellId);
|
|
16965
|
+
if (!await this.ensureRepository(cell)) return [];
|
|
16966
|
+
const result2 = await cell.exec(`${GIT} log -n ${MAX_CHECKPOINTS} --format=%H%x1f%cI%x1f%s`);
|
|
16967
|
+
if (result2.exitCode !== 0) return [];
|
|
16968
|
+
return result2.stdout.split("\n").filter(Boolean).map((line) => {
|
|
16969
|
+
const [id = "", createdAt = "", message2 = ""] = line.split("");
|
|
16970
|
+
return { id, createdAt, message: message2 };
|
|
16971
|
+
});
|
|
16972
|
+
}
|
|
16973
|
+
/**
|
|
16974
|
+
* Make the workspace match a checkpoint, recorded as a new commit so the restore
|
|
16975
|
+
* itself can be undone. Ignored files (node_modules, .aster state) are untouched.
|
|
16976
|
+
*/
|
|
16977
|
+
async restoreCheckpoint(cellId, conversationId, checkpointId) {
|
|
16978
|
+
if (!COMMIT_SHA_RE.test(checkpointId)) throw new Error("invalid_checkpoint");
|
|
16979
|
+
const cell = await this.provider.getCell(cellId);
|
|
16980
|
+
if (!await this.ensureRepository(cell)) throw new Error("checkpoints_unavailable");
|
|
16981
|
+
const exists = await cell.exec(`${GIT} cat-file -e ${checkpointId}^{commit}`);
|
|
16982
|
+
if (exists.exitCode !== 0) throw new Error("checkpoint_not_found");
|
|
16983
|
+
const short = checkpointId.slice(0, 7);
|
|
16984
|
+
await cell.writeFile(CHECKPOINT_MESSAGE_FILE, `Restore checkpoint ${short}
|
|
16985
|
+
`);
|
|
16986
|
+
const restore = await cell.exec(
|
|
16987
|
+
`${GIT} restore --source=${checkpointId} --staged --worktree -- :/ && ${GIT} clean -fdq && ${GIT} commit -q --allow-empty -F ${CHECKPOINT_MESSAGE_FILE}`
|
|
16988
|
+
);
|
|
16989
|
+
if (restore.exitCode !== 0) throw new Error("checkpoint_restore_failed");
|
|
16990
|
+
void conversationId;
|
|
16991
|
+
await this.addNote(
|
|
16992
|
+
cell,
|
|
16993
|
+
`[The user restored the workspace to checkpoint ${short}. Files changed after that point were reverted; re-read files before relying on earlier observations.]`
|
|
16994
|
+
);
|
|
16995
|
+
const [head] = await this.listCheckpoints(cellId);
|
|
16996
|
+
if (!head) throw new Error("checkpoint_restore_failed");
|
|
16997
|
+
return head;
|
|
16998
|
+
}
|
|
15684
16999
|
startTurn(turn) {
|
|
15685
17000
|
void this.runTurn(turn).catch(() => {
|
|
15686
17001
|
turn.finish("failed", "cell_runtime_error");
|
|
15687
17002
|
});
|
|
15688
17003
|
}
|
|
17004
|
+
/**
|
|
17005
|
+
* Re-attach to a turn that was running when the server restarted. The cell kept working
|
|
17006
|
+
* and journaled its events, so this only resumes reading them from the saved cursor.
|
|
17007
|
+
*/
|
|
17008
|
+
resumeTurn(turn) {
|
|
17009
|
+
void this.runTurn(turn, { resume: true }).catch(() => {
|
|
17010
|
+
turn.finish("failed", "cell_runtime_error");
|
|
17011
|
+
});
|
|
17012
|
+
}
|
|
15689
17013
|
cancelRun(runId) {
|
|
15690
17014
|
const active = this.activeRuns.get(runId);
|
|
15691
17015
|
if (!active) return;
|
|
15692
17016
|
active.stop();
|
|
15693
|
-
void this.controlById(active.cellId, "POST", "/rpc", { id:
|
|
17017
|
+
void this.controlById(active.cellId, "POST", "/rpc", { id: randomUUID8(), type: "abort" }).catch(() => {
|
|
15694
17018
|
});
|
|
15695
17019
|
}
|
|
15696
|
-
async runTurn(turn) {
|
|
17020
|
+
async runTurn(turn, options = {}) {
|
|
15697
17021
|
const { cellId, runId } = turn;
|
|
15698
17022
|
let cursor = turn.cellEventCursor;
|
|
15699
17023
|
let stopped = false;
|
|
@@ -15708,12 +17032,32 @@ var init_aster_cell_runtime = __esm({
|
|
|
15708
17032
|
this.activeRuns.delete(runId);
|
|
15709
17033
|
turn.finish(status, reason);
|
|
15710
17034
|
};
|
|
17035
|
+
let outcome;
|
|
17036
|
+
let artifactsBefore = [];
|
|
15711
17037
|
try {
|
|
15712
|
-
|
|
15713
|
-
|
|
15714
|
-
|
|
15715
|
-
|
|
17038
|
+
if (options.resume) throw new Error("resume");
|
|
17039
|
+
await this.ensureReady(cellId);
|
|
17040
|
+
const cell = await this.provider.getCell(cellId);
|
|
17041
|
+
if (turn.memory) {
|
|
17042
|
+
await cell.mkdir(CELL_STATE_DIR, { parents: true });
|
|
17043
|
+
await writeCellFileChunked(cell, MEMORY_FILE, turn.memory.snapshot);
|
|
17044
|
+
}
|
|
17045
|
+
const note = [await this.readNotes(cell), turn.memory?.note ?? ""].filter(Boolean).join("\n\n");
|
|
17046
|
+
const message2 = note ? `${note}
|
|
17047
|
+
|
|
17048
|
+
${turn.userText}` : turn.userText;
|
|
17049
|
+
artifactsBefore = await this.listArtifacts(cellId).catch(() => []);
|
|
17050
|
+
await this.controlById(cellId, "POST", "/rpc", { id: runId, type: "prompt", message: message2 });
|
|
17051
|
+
if (note) await this.clearNotes(cell);
|
|
17052
|
+
} catch (error) {
|
|
17053
|
+
if (!(error instanceof Error && error.message === "resume")) {
|
|
17054
|
+
finish("failed", "cell_prompt_rejected");
|
|
17055
|
+
return;
|
|
17056
|
+
}
|
|
17057
|
+
artifactsBefore = await this.listArtifacts(cellId).catch(() => []);
|
|
15716
17058
|
}
|
|
17059
|
+
let quietPolls = 0;
|
|
17060
|
+
const quietLimit = Math.max(5, Math.ceil(LIVENESS_CHECK_MS / this.pollIntervalMs));
|
|
15717
17061
|
while (!stopped) {
|
|
15718
17062
|
await sleep4(this.pollIntervalMs);
|
|
15719
17063
|
if (stopped) break;
|
|
@@ -15724,6 +17068,18 @@ var init_aster_cell_runtime = __esm({
|
|
|
15724
17068
|
} catch {
|
|
15725
17069
|
continue;
|
|
15726
17070
|
}
|
|
17071
|
+
if (events.length === 0) {
|
|
17072
|
+
if (++quietPolls >= quietLimit) {
|
|
17073
|
+
quietPolls = 0;
|
|
17074
|
+
const status = await this.controlById(cellId, "GET", "/status").catch(() => void 0);
|
|
17075
|
+
if (status && !status.activeRunId) {
|
|
17076
|
+
outcome = { status: "failed", reason: "cell_turn_lost" };
|
|
17077
|
+
break;
|
|
17078
|
+
}
|
|
17079
|
+
}
|
|
17080
|
+
continue;
|
|
17081
|
+
}
|
|
17082
|
+
quietPolls = 0;
|
|
15727
17083
|
for (const entry of events) {
|
|
15728
17084
|
cursor = Math.max(cursor, entry.cursor);
|
|
15729
17085
|
turn.onCellCursor?.(cursor);
|
|
@@ -15736,35 +17092,72 @@ var init_aster_cell_runtime = __esm({
|
|
|
15736
17092
|
if (typeof event.text === "string") turn.publishEvent("assistant.reasoning", { text: event.text });
|
|
15737
17093
|
break;
|
|
15738
17094
|
case "tool.start":
|
|
15739
|
-
turn.publishEvent("tool.activity", {
|
|
17095
|
+
turn.publishEvent("tool.activity", {
|
|
17096
|
+
tool: event.toolName,
|
|
17097
|
+
toolCallId: event.toolCallId,
|
|
17098
|
+
activity: "started",
|
|
17099
|
+
input: summarizeToolInput(event.toolName, event.args)
|
|
17100
|
+
});
|
|
17101
|
+
break;
|
|
17102
|
+
case "tool.result":
|
|
17103
|
+
turn.publishEvent("tool.activity", {
|
|
17104
|
+
tool: event.toolName,
|
|
17105
|
+
toolCallId: event.toolCallId,
|
|
17106
|
+
activity: event.isError === true ? "failed" : "completed",
|
|
17107
|
+
output: boundedToolText(typeof event.result === "string" ? event.result : "", MAX_TOOL_OUTPUT_CHARS)
|
|
17108
|
+
});
|
|
15740
17109
|
break;
|
|
15741
17110
|
case "permission.request": {
|
|
15742
|
-
const
|
|
15743
|
-
tool: { name: event.
|
|
17111
|
+
const decision = await turn.askPermission({
|
|
17112
|
+
tool: { name: event.toolName },
|
|
15744
17113
|
args: event.args ?? {}
|
|
15745
17114
|
});
|
|
15746
17115
|
await this.controlById(cellId, "POST", "/rpc", {
|
|
15747
|
-
id:
|
|
17116
|
+
id: randomUUID8(),
|
|
15748
17117
|
type: "resolve_permission",
|
|
15749
|
-
|
|
15750
|
-
|
|
17118
|
+
requestId: event.requestId,
|
|
17119
|
+
decision
|
|
15751
17120
|
}).catch(() => {
|
|
15752
17121
|
});
|
|
15753
|
-
turn.publishEvent("tool.activity", { tool: event.
|
|
17122
|
+
turn.publishEvent("tool.activity", { tool: event.toolName, activity: decision === "allow" ? "approved" : "rejected" });
|
|
15754
17123
|
break;
|
|
15755
17124
|
}
|
|
15756
17125
|
case "usage":
|
|
15757
17126
|
turn.publishEvent("usage", { usage: event.usage });
|
|
15758
17127
|
break;
|
|
15759
17128
|
case "session.end":
|
|
15760
|
-
|
|
17129
|
+
outcome = {
|
|
17130
|
+
status: event.reason === "aborted" ? "cancelled" : event.reason === "complete" ? "completed" : "failed",
|
|
17131
|
+
// Surface the agent's actual error (redacted, bounded) instead of a bare "error".
|
|
17132
|
+
reason: event.reason === "error" && typeof event.error === "string" && event.error.trim() ? boundedToolText(event.error.trim(), 500) : String(event.reason ?? "cell_session_ended")
|
|
17133
|
+
};
|
|
17134
|
+
stopped = true;
|
|
15761
17135
|
break;
|
|
15762
17136
|
default:
|
|
15763
17137
|
break;
|
|
15764
17138
|
}
|
|
17139
|
+
if (outcome) break;
|
|
15765
17140
|
}
|
|
15766
17141
|
}
|
|
15767
|
-
|
|
17142
|
+
try {
|
|
17143
|
+
const before = new Map(artifactsBefore.map((artifact) => [artifact.path, artifact]));
|
|
17144
|
+
for (const artifact of await this.listArtifacts(cellId)) {
|
|
17145
|
+
const previous = before.get(artifact.path);
|
|
17146
|
+
if (!previous || previous.size !== artifact.size || previous.modifiedAt !== artifact.modifiedAt) {
|
|
17147
|
+
turn.publishEvent("artifact.updated", { ...artifact });
|
|
17148
|
+
}
|
|
17149
|
+
}
|
|
17150
|
+
} catch {
|
|
17151
|
+
}
|
|
17152
|
+
await this.checkpoint(cellId, turn.userText).then((checkpoint) => {
|
|
17153
|
+
if (checkpoint) turn.publishEvent("checkpoint.created", { ...checkpoint });
|
|
17154
|
+
}).catch(() => {
|
|
17155
|
+
});
|
|
17156
|
+
if (outcome?.status === "completed" && turn.beforeFinish) {
|
|
17157
|
+
await Promise.race([turn.beforeFinish().catch(() => {
|
|
17158
|
+
}), sleep4(MEMORY_EXTRACTION_TIMEOUT_MS)]);
|
|
17159
|
+
}
|
|
17160
|
+
finish(outcome?.status ?? "cancelled", outcome?.reason ?? "cell_turn_stopped");
|
|
15768
17161
|
}
|
|
15769
17162
|
async seedWorkspace(cell, workspaceRoot) {
|
|
15770
17163
|
const seeded = await cell.exec(`test -f ${CELL_STATE_DIR}/.seeded`);
|
|
@@ -15776,15 +17169,112 @@ var init_aster_cell_runtime = __esm({
|
|
|
15776
17169
|
if (extract.exitCode !== 0) throw new Error("workspace_seed_failed");
|
|
15777
17170
|
await cell.exec(`rm -f ${CELL_STATE_DIR}/workspace.tar.b64 && touch ${CELL_STATE_DIR}/.seeded`);
|
|
15778
17171
|
}
|
|
17172
|
+
/**
|
|
17173
|
+
* Make /workspace a git repository with an initial commit (idempotent), excluding the
|
|
17174
|
+
* bridge's private .aster state. Returns false when the cell image has no git, in
|
|
17175
|
+
* which case checkpoints are skipped rather than failing the conversation.
|
|
17176
|
+
*/
|
|
17177
|
+
async ensureRepository(cell, excludeArtifacts = false) {
|
|
17178
|
+
const result2 = await cell.exec(
|
|
17179
|
+
`command -v git >/dev/null || exit 3; if [ ! -d ${CELL_WORKSPACE}/.git ]; then git init -q ${CELL_WORKSPACE} || exit 1; fi; grep -qx '.aster/' ${CELL_WORKSPACE}/.git/info/exclude 2>/dev/null || printf '.aster/\\n' >> ${CELL_WORKSPACE}/.git/info/exclude; ` + // A code cell's artifacts are for the user, not the repository.
|
|
17180
|
+
(excludeArtifacts ? `grep -qx 'artifacts/' ${CELL_WORKSPACE}/.git/info/exclude || printf 'artifacts/\\n' >> ${CELL_WORKSPACE}/.git/info/exclude; ` : "") + `${GIT} rev-parse -q --verify HEAD >/dev/null || { ${GIT} add -A && ${GIT} commit -q --allow-empty -m 'Workspace created'; }`
|
|
17181
|
+
);
|
|
17182
|
+
return result2.exitCode === 0;
|
|
17183
|
+
}
|
|
17184
|
+
/** Commit all workspace changes; returns the new checkpoint, or undefined when nothing changed. */
|
|
17185
|
+
async checkpoint(cellId, userText) {
|
|
17186
|
+
const cell = await this.provider.getCell(cellId);
|
|
17187
|
+
if (!await this.ensureRepository(cell)) return void 0;
|
|
17188
|
+
await cell.writeFile(CHECKPOINT_MESSAGE_FILE, checkpointMessage(userText), { mode: "0600" });
|
|
17189
|
+
const commit = await cell.exec(
|
|
17190
|
+
`${GIT} add -A && if ${GIT} diff --cached --quiet; then exit 4; fi && ${GIT} commit -q -F ${CHECKPOINT_MESSAGE_FILE}`
|
|
17191
|
+
);
|
|
17192
|
+
if (commit.exitCode !== 0) return void 0;
|
|
17193
|
+
const [head] = await this.listCheckpoints(cellId);
|
|
17194
|
+
return head;
|
|
17195
|
+
}
|
|
17196
|
+
/**
|
|
17197
|
+
* Install the Autopilot runtime unless the cell already has the expected version. This
|
|
17198
|
+
* checks the installed package itself rather than a marker on the persistent volume:
|
|
17199
|
+
* /opt lives in the cell's root filesystem, which a container-driver pause discards,
|
|
17200
|
+
* while an image with the runtime baked in skips the install entirely.
|
|
17201
|
+
*/
|
|
17202
|
+
/**
|
|
17203
|
+
* Clone a GitHub repository into /workspace on a new working branch, through Hotcell's
|
|
17204
|
+
* credential gateway: the remote points at the gateway and git authenticates with the
|
|
17205
|
+
* cell's own egress token, so the real GitHub token never enters the cell. Idempotent.
|
|
17206
|
+
*/
|
|
17207
|
+
async cloneRepository(cell, project) {
|
|
17208
|
+
const seeded = await cell.exec(`test -f ${CELL_STATE_DIR}/.seeded`);
|
|
17209
|
+
if (seeded.exitCode === 0) return;
|
|
17210
|
+
const { repository, baseBranch, branch } = project;
|
|
17211
|
+
if (!repository || !REPOSITORY_RE.test(repository) || !baseBranch || !BRANCH_RE.test(baseBranch) || !branch || !BRANCH_RE.test(branch)) {
|
|
17212
|
+
throw new Error("invalid_project");
|
|
17213
|
+
}
|
|
17214
|
+
await cell.mkdir(CELL_STATE_DIR, { parents: true });
|
|
17215
|
+
const clone = await cell.exec(
|
|
17216
|
+
`set -e; cd ${CELL_WORKSPACE}; if [ -n "$ASTER_GITHUB_GIT_URL" ]; then remote="$ASTER_GITHUB_GIT_URL/${repository}.git"; else remote="\${GITHUB_BASE_URL%/github}/github-git/${repository}.git"; fi; git init -q; git remote add origin "$remote" 2>/dev/null || git remote set-url origin "$remote"; git config credential.helper '!f() { echo username=x-access-token; echo "password=\${ASTER_GITHUB_TOKEN:-$GITHUB_API_KEY}"; }; f'; git config user.name "Aster Autopilot"; git config user.email "autopilot@aster.invalid"; if git ls-remote --exit-code --heads origin '${baseBranch}' >/dev/null 2>&1; then git fetch -q origin '${baseBranch}'; git checkout -q -b '${branch}' FETCH_HEAD; else git checkout -q -b '${branch}'; fi; touch ${CELL_STATE_DIR}/.seeded`
|
|
17217
|
+
);
|
|
17218
|
+
if (clone.exitCode !== 0) throw new Error("repository_clone_failed");
|
|
17219
|
+
}
|
|
17220
|
+
/** Files under /workspace/artifacts, the folder the agent saves user-facing deliverables in. */
|
|
17221
|
+
async listArtifacts(cellId) {
|
|
17222
|
+
const cell = await this.provider.getCell(cellId);
|
|
17223
|
+
const result2 = await cell.exec(
|
|
17224
|
+
`[ -d ${ARTIFACTS_DIR} ] || exit 0; cd ${ARTIFACTS_DIR} && find . -type f ! -path '*/.*' -printf '%P\\t%s\\t%T@\\n' | head -n ${MAX_ARTIFACTS}`
|
|
17225
|
+
);
|
|
17226
|
+
if (result2.exitCode !== 0) return [];
|
|
17227
|
+
return result2.stdout.split("\n").filter(Boolean).flatMap((line) => {
|
|
17228
|
+
const [path = "", size = "", modified = ""] = line.split(" ");
|
|
17229
|
+
if (!isSafeArtifactPath(path)) return [];
|
|
17230
|
+
return [{
|
|
17231
|
+
path,
|
|
17232
|
+
name: path.split("/").pop() ?? path,
|
|
17233
|
+
size: Number(size) || 0,
|
|
17234
|
+
modifiedAt: new Date(Math.round(Number(modified) * 1e3) || Date.now()).toISOString(),
|
|
17235
|
+
mimeType: artifactMimeType(path)
|
|
17236
|
+
}];
|
|
17237
|
+
}).sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt));
|
|
17238
|
+
}
|
|
17239
|
+
/** The bytes of one artifact, at most 50 MB. */
|
|
17240
|
+
async readArtifact(cellId, path) {
|
|
17241
|
+
if (!isSafeArtifactPath(path)) throw new Error("invalid_artifact_path");
|
|
17242
|
+
const cell = await this.provider.getCell(cellId);
|
|
17243
|
+
const file = shellQuote2(`${ARTIFACTS_DIR}/${path}`);
|
|
17244
|
+
const stat10 = await cell.exec(`[ -f ${file} ] && [ ! -L ${file} ] && stat -c %s -- ${file}`);
|
|
17245
|
+
if (stat10.exitCode !== 0) throw new Error("artifact_not_found");
|
|
17246
|
+
if (Number(stat10.stdout.trim()) > MAX_ARTIFACT_BYTES) throw new Error("artifact_too_large");
|
|
17247
|
+
const encoded = await cell.exec(`base64 -w0 -- ${file}`);
|
|
17248
|
+
if (encoded.exitCode !== 0) throw new Error("artifact_not_found");
|
|
17249
|
+
return { data: Buffer.from(encoded.stdout.trim(), "base64"), mimeType: artifactMimeType(path), name: path.split("/").pop() ?? path };
|
|
17250
|
+
}
|
|
17251
|
+
/** Notes are prepended to the agent's next prompt once; kept in the cell so they survive restarts. */
|
|
17252
|
+
async addNote(cell, text) {
|
|
17253
|
+
const existing = await this.readNotes(cell);
|
|
17254
|
+
await cell.mkdir(CELL_STATE_DIR, { parents: true });
|
|
17255
|
+
await cell.writeFile(NOTES_FILE, existing ? `${existing}
|
|
17256
|
+
|
|
17257
|
+
${text}` : text, { mode: "0600" });
|
|
17258
|
+
}
|
|
17259
|
+
async readNotes(cell) {
|
|
17260
|
+
try {
|
|
17261
|
+
return (await cell.readFile(NOTES_FILE)).trim();
|
|
17262
|
+
} catch {
|
|
17263
|
+
return "";
|
|
17264
|
+
}
|
|
17265
|
+
}
|
|
17266
|
+
async clearNotes(cell) {
|
|
17267
|
+
await cell.exec(`rm -f ${NOTES_FILE}`);
|
|
17268
|
+
}
|
|
15779
17269
|
async installRuntime(cell) {
|
|
15780
|
-
const
|
|
17270
|
+
const expectedVersion = /@(\d[^@/]*)$/.exec(this.packageSpec)?.[1];
|
|
15781
17271
|
try {
|
|
15782
|
-
|
|
17272
|
+
const installed = JSON.parse(await cell.readFile(INSTALLED_RUNTIME_PACKAGE));
|
|
17273
|
+
if (!expectedVersion || installed.version === expectedVersion) return;
|
|
15783
17274
|
} catch {
|
|
15784
17275
|
}
|
|
15785
17276
|
const install = await cell.exec(`mkdir -p /opt/autopilot && npm install --prefix /opt/autopilot --no-audit --no-fund ${this.packageSpec}`);
|
|
15786
17277
|
if (install.exitCode !== 0) throw new Error("cell_runtime_install_failed");
|
|
15787
|
-
await cell.writeFile(marker, this.packageSpec);
|
|
15788
17278
|
}
|
|
15789
17279
|
async ensureBridge(cell) {
|
|
15790
17280
|
const processes = await cell.listProcesses().catch(() => []);
|
|
@@ -15797,7 +17287,7 @@ var init_aster_cell_runtime = __esm({
|
|
|
15797
17287
|
for (; ; ) {
|
|
15798
17288
|
try {
|
|
15799
17289
|
const health = await this.control(cell, "GET", "/health");
|
|
15800
|
-
if (health.ok === true) return;
|
|
17290
|
+
if (health.ok === true && (!health.sessionId || health.initialized === true)) return;
|
|
15801
17291
|
} catch {
|
|
15802
17292
|
}
|
|
15803
17293
|
if (Date.now() > deadline) throw new Error("cell_bridge_unhealthy");
|
|
@@ -15805,9 +17295,9 @@ var init_aster_cell_runtime = __esm({
|
|
|
15805
17295
|
}
|
|
15806
17296
|
}
|
|
15807
17297
|
async control(cell, method, path, body) {
|
|
15808
|
-
const requestId =
|
|
17298
|
+
const requestId = randomUUID8();
|
|
15809
17299
|
await cell.mkdir(CELL_CONTROL_DIR, { parents: true });
|
|
15810
|
-
await cell
|
|
17300
|
+
await writeCellFileChunked(cell, `${CELL_CONTROL_DIR}/${requestId}.json`, JSON.stringify({ method, path, body }));
|
|
15811
17301
|
const result2 = await cell.exec(`${CONTROL_RUNNER} ${requestId}`);
|
|
15812
17302
|
if (result2.exitCode !== 0) throw new Error("cell_control_failed");
|
|
15813
17303
|
const parsed = JSON.parse(result2.stdout.trim().split("\n").pop() ?? "{}");
|
|
@@ -15819,6 +17309,32 @@ var init_aster_cell_runtime = __esm({
|
|
|
15819
17309
|
return this.control(cell, method, path, body);
|
|
15820
17310
|
}
|
|
15821
17311
|
};
|
|
17312
|
+
CELL_FILE_WRITE_CHUNK_CHARS = 32 * 1024;
|
|
17313
|
+
ARTIFACT_MIME_TYPES = {
|
|
17314
|
+
pdf: "application/pdf",
|
|
17315
|
+
html: "text/html",
|
|
17316
|
+
htm: "text/html",
|
|
17317
|
+
md: "text/markdown",
|
|
17318
|
+
txt: "text/plain",
|
|
17319
|
+
csv: "text/csv",
|
|
17320
|
+
json: "application/json",
|
|
17321
|
+
png: "image/png",
|
|
17322
|
+
jpg: "image/jpeg",
|
|
17323
|
+
jpeg: "image/jpeg",
|
|
17324
|
+
gif: "image/gif",
|
|
17325
|
+
webp: "image/webp",
|
|
17326
|
+
svg: "image/svg+xml",
|
|
17327
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
17328
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
17329
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
17330
|
+
key: "application/vnd.apple.keynote",
|
|
17331
|
+
pages: "application/vnd.apple.pages",
|
|
17332
|
+
numbers: "application/vnd.apple.numbers",
|
|
17333
|
+
zip: "application/zip",
|
|
17334
|
+
mp4: "video/mp4",
|
|
17335
|
+
mov: "video/quicktime",
|
|
17336
|
+
mp3: "audio/mpeg"
|
|
17337
|
+
};
|
|
15822
17338
|
}
|
|
15823
17339
|
});
|
|
15824
17340
|
|
|
@@ -15831,8 +17347,8 @@ function validateConfig(config2) {
|
|
|
15831
17347
|
throw new HotcellProviderError("Hotcell must use HTTPS unless the control endpoint is loopback", "insecure_hotcell_endpoint");
|
|
15832
17348
|
}
|
|
15833
17349
|
if (!config2.apiKey.trim()) throw new HotcellProviderError("Hotcell API key is required", "hotcell_auth_required");
|
|
15834
|
-
if (
|
|
15835
|
-
throw new HotcellProviderError("Aster requires an explicit Hotcell
|
|
17350
|
+
if (!HOTCELL_DRIVERS.includes(config2.driver)) {
|
|
17351
|
+
throw new HotcellProviderError("Aster requires an explicit Hotcell driver", "unsafe_hotcell_driver");
|
|
15836
17352
|
}
|
|
15837
17353
|
if (!Number.isInteger(config2.memoryMb) || config2.memoryMb < 512 || !Number.isFinite(config2.cpus) || config2.cpus < 0.5 || !Number.isInteger(config2.pidsLimit) || config2.pidsLimit < 32) {
|
|
15838
17354
|
throw new HotcellProviderError("Hotcell resource limits are invalid", "invalid_hotcell_limits");
|
|
@@ -15856,11 +17372,12 @@ function numberEnv(value, fallback, min, max) {
|
|
|
15856
17372
|
if (!Number.isFinite(parsed) || parsed < min || parsed > max) throw new HotcellProviderError("Invalid Hotcell resource configuration", "invalid_hotcell_config");
|
|
15857
17373
|
return parsed;
|
|
15858
17374
|
}
|
|
15859
|
-
var HOTCELL_CONVERSATION_LABEL, HotcellProviderError, HotcellProvider;
|
|
17375
|
+
var HOTCELL_CONVERSATION_LABEL, HOTCELL_DRIVERS, HotcellProviderError, HotcellProvider;
|
|
15860
17376
|
var init_hotcell_provider = __esm({
|
|
15861
17377
|
"src/server/hotcell-provider.ts"() {
|
|
15862
17378
|
"use strict";
|
|
15863
17379
|
HOTCELL_CONVERSATION_LABEL = "autopilot.conversation_id";
|
|
17380
|
+
HOTCELL_DRIVERS = ["applevz", "firecracker", "container"];
|
|
15864
17381
|
HotcellProviderError = class extends Error {
|
|
15865
17382
|
constructor(message2, code) {
|
|
15866
17383
|
super(message2);
|
|
@@ -15881,9 +17398,9 @@ var init_hotcell_provider = __esm({
|
|
|
15881
17398
|
const endpoint = env2.AUTOPILOT_HOTCELL_ENDPOINT;
|
|
15882
17399
|
const apiKey = env2.AUTOPILOT_HOTCELL_API_KEY;
|
|
15883
17400
|
const driver = env2.AUTOPILOT_HOTCELL_DRIVER;
|
|
15884
|
-
if (!endpoint || !apiKey || driver
|
|
17401
|
+
if (!endpoint || !apiKey || !HOTCELL_DRIVERS.includes(driver)) {
|
|
15885
17402
|
throw new HotcellProviderError(
|
|
15886
|
-
"Aster Hotcell requires AUTOPILOT_HOTCELL_ENDPOINT, AUTOPILOT_HOTCELL_API_KEY, and an explicit AUTOPILOT_HOTCELL_DRIVER (applevz or
|
|
17403
|
+
"Aster Hotcell requires AUTOPILOT_HOTCELL_ENDPOINT, AUTOPILOT_HOTCELL_API_KEY, and an explicit AUTOPILOT_HOTCELL_DRIVER (applevz, firecracker, or container)",
|
|
15887
17404
|
"hotcell_not_configured"
|
|
15888
17405
|
);
|
|
15889
17406
|
}
|
|
@@ -15891,6 +17408,11 @@ var init_hotcell_provider = __esm({
|
|
|
15891
17408
|
endpoint,
|
|
15892
17409
|
apiKey,
|
|
15893
17410
|
driver,
|
|
17411
|
+
networked: env2.AUTOPILOT_HOTCELL_NETWORKED === "true",
|
|
17412
|
+
anyModel: env2.AUTOPILOT_HOTCELL_ANY_MODEL === "true",
|
|
17413
|
+
sleepAfterMs: integerEnv(env2.AUTOPILOT_HOTCELL_SLEEP_AFTER_MS, 0, 0, 7 * 24 * 60 * 60 * 1e3),
|
|
17414
|
+
...env2.AUTOPILOT_HOTCELL_IMAGE ? { image: env2.AUTOPILOT_HOTCELL_IMAGE } : {},
|
|
17415
|
+
...env2.AUTOPILOT_HOTCELL_CHAT_MEMORY_MB ? { chatMemoryMb: integerEnv(env2.AUTOPILOT_HOTCELL_CHAT_MEMORY_MB, 1024, 512, 65536) } : {},
|
|
15894
17416
|
memoryMb: integerEnv(env2.AUTOPILOT_HOTCELL_MEMORY_MB, 4096, 512, 65536),
|
|
15895
17417
|
cpus: numberEnv(env2.AUTOPILOT_HOTCELL_CPUS, 2, 0.5, 64),
|
|
15896
17418
|
pidsLimit: integerEnv(env2.AUTOPILOT_HOTCELL_PIDS, 256, 32, 8192),
|
|
@@ -15915,23 +17437,28 @@ var init_hotcell_provider = __esm({
|
|
|
15915
17437
|
const info = await this.client.info();
|
|
15916
17438
|
if (!info.auth) throw new HotcellProviderError("Hotcell API authentication is disabled; refusing to provision an Aster cell", "hotcell_auth_required");
|
|
15917
17439
|
if (!info.drivers.includes(this.config.driver)) {
|
|
15918
|
-
throw new HotcellProviderError("Configured Hotcell
|
|
17440
|
+
throw new HotcellProviderError("Configured Hotcell driver is unavailable", "hotcell_driver_unavailable");
|
|
15919
17441
|
}
|
|
15920
17442
|
if (!info.egressProviders.includes("openrouter")) {
|
|
15921
17443
|
throw new HotcellProviderError("Hotcell OpenRouter credential gateway is unavailable", "hotcell_egress_unavailable");
|
|
15922
17444
|
}
|
|
17445
|
+
if (input.kind === "code" && !input.githubViaProxy && !info.egressProviders.includes("github")) {
|
|
17446
|
+
throw new HotcellProviderError("Hotcell GitHub credential gateway is unavailable", "hotcell_github_unavailable");
|
|
17447
|
+
}
|
|
15923
17448
|
const options = {
|
|
15924
17449
|
driver: this.config.driver,
|
|
15925
|
-
|
|
17450
|
+
...this.config.image ? { image: this.config.image } : {},
|
|
17451
|
+
...input.env ? { env: input.env } : {},
|
|
17452
|
+
networked: this.config.networked === true,
|
|
15926
17453
|
persist: true,
|
|
15927
|
-
sleepAfter: 0,
|
|
15928
|
-
memoryMb: this.config.memoryMb,
|
|
17454
|
+
sleepAfter: this.config.sleepAfterMs ?? 0,
|
|
17455
|
+
memoryMb: input.kind === "chat" ? this.config.chatMemoryMb ?? this.config.memoryMb : this.config.memoryMb,
|
|
15929
17456
|
cpus: this.config.cpus,
|
|
15930
17457
|
pidsLimit: this.config.pidsLimit,
|
|
15931
17458
|
egressSpendCapUsd: this.config.spendCapUsd,
|
|
15932
17459
|
egress: {
|
|
15933
|
-
providers: ["openrouter"],
|
|
15934
|
-
models: [input.model],
|
|
17460
|
+
providers: input.kind === "code" && !input.githubViaProxy ? ["openrouter", "github", "github-git"] : ["openrouter"],
|
|
17461
|
+
...this.config.anyModel ? {} : { models: [input.model] },
|
|
15935
17462
|
spendCapUsd: this.config.spendCapUsd,
|
|
15936
17463
|
ttlMs: this.config.tokenTtlMs
|
|
15937
17464
|
},
|
|
@@ -15961,6 +17488,15 @@ var init_hotcell_provider = __esm({
|
|
|
15961
17488
|
for (const token of tokens) await cell.revokeEgressToken(token.token);
|
|
15962
17489
|
await cell.destroy();
|
|
15963
17490
|
}
|
|
17491
|
+
/** Aster conversation cells with their lifecycle status and last API activity. */
|
|
17492
|
+
async listConversationCells() {
|
|
17493
|
+
return (await this.client.list()).filter((item) => item.labels?.[HOTCELL_CONVERSATION_LABEL]).map((item) => ({
|
|
17494
|
+
id: item.id,
|
|
17495
|
+
conversationId: item.labels[HOTCELL_CONVERSATION_LABEL],
|
|
17496
|
+
status: item.status,
|
|
17497
|
+
lastActivityAt: item.lastActivityAt
|
|
17498
|
+
}));
|
|
17499
|
+
}
|
|
15964
17500
|
/** True suspend: on microVM drivers the daemon snapshots memory, so the Autopilot session resumes alive. */
|
|
15965
17501
|
async pauseConversationCell(cellId) {
|
|
15966
17502
|
if (!cellId.trim()) throw new HotcellProviderError("Missing Hotcell id", "hotcell_id_missing");
|
|
@@ -15975,13 +17511,16 @@ var init_hotcell_provider = __esm({
|
|
|
15975
17511
|
});
|
|
15976
17512
|
|
|
15977
17513
|
// src/server/routes.ts
|
|
15978
|
-
import { randomUUID as
|
|
17514
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
15979
17515
|
import { URL as URL2 } from "url";
|
|
15980
|
-
import { readFile as
|
|
17516
|
+
import { readFile as readFile21, unlink as unlink3 } from "fs/promises";
|
|
15981
17517
|
import { resolve as resolve9, basename as basename5 } from "path";
|
|
15982
17518
|
function createHotcellAsterRuntime() {
|
|
15983
17519
|
try {
|
|
15984
|
-
|
|
17520
|
+
const idlePauseMs = Number(process.env.AUTOPILOT_ASTER_IDLE_PAUSE_MS ?? 15 * 60 * 1e3);
|
|
17521
|
+
return new AsterHotcellRuntime(HotcellProvider.fromEnvironment(), {
|
|
17522
|
+
idlePauseMs: Number.isFinite(idlePauseMs) && idlePauseMs >= 0 ? idlePauseMs : 15 * 60 * 1e3
|
|
17523
|
+
});
|
|
15985
17524
|
} catch (error) {
|
|
15986
17525
|
if (error instanceof HotcellProviderError && error.code === "hotcell_not_configured") return void 0;
|
|
15987
17526
|
throw error;
|
|
@@ -16067,7 +17606,7 @@ async function buildUserMessage2(prompt, files, cwd) {
|
|
|
16067
17606
|
}
|
|
16068
17607
|
} else {
|
|
16069
17608
|
try {
|
|
16070
|
-
const content = await
|
|
17609
|
+
const content = await readFile21(filePath, "utf8");
|
|
16071
17610
|
const relPath = filePath.startsWith(cwd) ? filePath.slice(cwd.length + 1) : filePath;
|
|
16072
17611
|
fileContents.push(`
|
|
16073
17612
|
--- ${relPath} ---
|
|
@@ -16146,6 +17685,12 @@ function setupRoutes(config2, options = {}) {
|
|
|
16146
17685
|
cancelRun: () => {
|
|
16147
17686
|
}
|
|
16148
17687
|
});
|
|
17688
|
+
if (!options.asterRuntime) {
|
|
17689
|
+
void asterApi.startGitHubProxy().catch((error) => {
|
|
17690
|
+
process.stderr.write(`aster: GitHub proxy failed to start: ${error instanceof Error ? error.message : String(error)}
|
|
17691
|
+
`);
|
|
17692
|
+
});
|
|
17693
|
+
}
|
|
16149
17694
|
const wakeScheduler = new RunWakeScheduler({
|
|
16150
17695
|
onWake: async (event) => {
|
|
16151
17696
|
const store = new RunStore();
|
|
@@ -16288,7 +17833,7 @@ function setupRoutes(config2, options = {}) {
|
|
|
16288
17833
|
const executor = new ToolExecutor(tools);
|
|
16289
17834
|
const { makeSessionId: makeSessionId2 } = await Promise.resolve().then(() => (init_sessions(), sessions_exports));
|
|
16290
17835
|
const sessionId = makeSessionId2(task);
|
|
16291
|
-
const runId =
|
|
17836
|
+
const runId = randomUUID9();
|
|
16292
17837
|
const worktreeManager = new RunWorktreeManager();
|
|
16293
17838
|
let worktree;
|
|
16294
17839
|
if (useWorktree) {
|
|
@@ -16870,10 +18415,10 @@ var server_exports = {};
|
|
|
16870
18415
|
__export(server_exports, {
|
|
16871
18416
|
startServer: () => startServer
|
|
16872
18417
|
});
|
|
16873
|
-
import { createServer } from "http";
|
|
18418
|
+
import { createServer as createServer2 } from "http";
|
|
16874
18419
|
async function startServer(opts2) {
|
|
16875
18420
|
const { handleRequest, cleanup } = setupRoutes(opts2.config, { asterRuntime: opts2.asterRuntime });
|
|
16876
|
-
const server =
|
|
18421
|
+
const server = createServer2((req, res) => {
|
|
16877
18422
|
const isAsterApi = req.url?.startsWith("/api/v1/") === true;
|
|
16878
18423
|
if (!isAsterApi) {
|
|
16879
18424
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -16972,9 +18517,9 @@ __export(db_exports, {
|
|
|
16972
18517
|
updateAccessedAt: () => updateAccessedAt,
|
|
16973
18518
|
updateMemoryEmbedding: () => updateMemoryEmbedding
|
|
16974
18519
|
});
|
|
16975
|
-
import
|
|
16976
|
-
import { dirname as
|
|
16977
|
-
import { mkdirSync as
|
|
18520
|
+
import Database6 from "better-sqlite3";
|
|
18521
|
+
import { dirname as dirname17 } from "path";
|
|
18522
|
+
import { mkdirSync as mkdirSync8, statSync as statSync4 } from "fs";
|
|
16978
18523
|
function initSchema(db) {
|
|
16979
18524
|
db.exec(`
|
|
16980
18525
|
CREATE TABLE IF NOT EXISTS memories (
|
|
@@ -17058,8 +18603,8 @@ function openMemoryDb(dbPath) {
|
|
|
17058
18603
|
if (dbInstance) {
|
|
17059
18604
|
dbInstance.close();
|
|
17060
18605
|
}
|
|
17061
|
-
|
|
17062
|
-
dbInstance = new
|
|
18606
|
+
mkdirSync8(dirname17(dbPath), { recursive: true });
|
|
18607
|
+
dbInstance = new Database6(dbPath);
|
|
17063
18608
|
dbInstance.pragma("journal_mode = WAL");
|
|
17064
18609
|
dbInstance.pragma("foreign_keys = ON");
|
|
17065
18610
|
initSchema(dbInstance);
|
|
@@ -19013,120 +20558,6 @@ var init_lsp = __esm({
|
|
|
19013
20558
|
}
|
|
19014
20559
|
});
|
|
19015
20560
|
|
|
19016
|
-
// src/models/openrouter-catalog.ts
|
|
19017
|
-
var openrouter_catalog_exports = {};
|
|
19018
|
-
__export(openrouter_catalog_exports, {
|
|
19019
|
-
DEFAULT_CACHE_TTL_MS: () => DEFAULT_CACHE_TTL_MS,
|
|
19020
|
-
ensureOpenRouterCatalog: () => ensureOpenRouterCatalog,
|
|
19021
|
-
fetchOpenRouterCatalog: () => fetchOpenRouterCatalog,
|
|
19022
|
-
loadOpenRouterCatalog: () => loadOpenRouterCatalog,
|
|
19023
|
-
mapOpenRouterModel: () => mapOpenRouterModel
|
|
19024
|
-
});
|
|
19025
|
-
import { readFile as readFile21, writeFile as writeFile12, mkdir as mkdir13 } from "fs/promises";
|
|
19026
|
-
import { homedir as homedir22 } from "os";
|
|
19027
|
-
import { join as join31, dirname as dirname16 } from "path";
|
|
19028
|
-
function catalogCachePath() {
|
|
19029
|
-
const xdg = process.env.XDG_CONFIG_HOME || join31(homedir22(), ".config");
|
|
19030
|
-
return join31(xdg, "kimiflare", "openrouter-models.json");
|
|
19031
|
-
}
|
|
19032
|
-
function perMtok(perToken) {
|
|
19033
|
-
const n = Number(perToken);
|
|
19034
|
-
return Number.isFinite(n) ? n * 1e6 : 0;
|
|
19035
|
-
}
|
|
19036
|
-
function mapOpenRouterModel(raw) {
|
|
19037
|
-
const supported = new Set(raw.supported_parameters ?? []);
|
|
19038
|
-
const aa = raw.benchmarks?.artificial_analysis;
|
|
19039
|
-
const quality = {
|
|
19040
|
-
...typeof aa?.coding_index === "number" ? { coding: aa.coding_index } : {},
|
|
19041
|
-
...typeof aa?.agentic_index === "number" ? { agentic: aa.agentic_index } : {},
|
|
19042
|
-
...typeof aa?.intelligence_index === "number" ? { intelligence: aa.intelligence_index } : {}
|
|
19043
|
-
};
|
|
19044
|
-
return {
|
|
19045
|
-
id: raw.id,
|
|
19046
|
-
...raw.name ? { name: raw.name } : {},
|
|
19047
|
-
...typeof raw.created === "number" ? { created: raw.created } : {},
|
|
19048
|
-
...Object.keys(quality).length > 0 ? { quality } : {},
|
|
19049
|
-
...raw.supported_parameters ? { parameters: [...raw.supported_parameters] } : {},
|
|
19050
|
-
contextWindow: raw.context_length ?? 128e3,
|
|
19051
|
-
maxOutputTokens: raw.top_provider?.max_completion_tokens ?? 4096,
|
|
19052
|
-
pricing: {
|
|
19053
|
-
inputPerMtok: perMtok(raw.pricing?.prompt),
|
|
19054
|
-
...raw.pricing?.input_cache_read ? { cachedInputPerMtok: perMtok(raw.pricing.input_cache_read) } : {},
|
|
19055
|
-
outputPerMtok: perMtok(raw.pricing?.completion)
|
|
19056
|
-
},
|
|
19057
|
-
supports: {
|
|
19058
|
-
// OpenRouter's own catalog is the authority on whether a model accepts tool
|
|
19059
|
-
// calls; absence of "tools" here means the upstream provider doesn't support
|
|
19060
|
-
// function calling for this model, and we must not claim otherwise.
|
|
19061
|
-
tools: supported.has("tools"),
|
|
19062
|
-
reasoning: supported.has("reasoning") || supported.has("include_reasoning"),
|
|
19063
|
-
streaming: true,
|
|
19064
|
-
vision: (raw.architecture?.input_modalities ?? []).includes("image"),
|
|
19065
|
-
temperature: supported.has("temperature") ? true : void 0
|
|
19066
|
-
}
|
|
19067
|
-
};
|
|
19068
|
-
}
|
|
19069
|
-
async function fetchOpenRouterCatalog(fetchImpl2 = fetch) {
|
|
19070
|
-
const res = await fetchWithNetworkRetry(fetchImpl2, openRouterUrl("models"), {
|
|
19071
|
-
headers: { Accept: "application/json" }
|
|
19072
|
-
});
|
|
19073
|
-
if (!res.ok) {
|
|
19074
|
-
throw new Error(`OpenRouter /models returned HTTP ${res.status}`);
|
|
19075
|
-
}
|
|
19076
|
-
const body = await res.json();
|
|
19077
|
-
return (body.data ?? []).map(mapOpenRouterModel);
|
|
19078
|
-
}
|
|
19079
|
-
async function readCache3() {
|
|
19080
|
-
try {
|
|
19081
|
-
const raw = await readFile21(catalogCachePath(), "utf8");
|
|
19082
|
-
const parsed = JSON.parse(raw);
|
|
19083
|
-
return parsed.version === CACHE_VERSION ? parsed : null;
|
|
19084
|
-
} catch {
|
|
19085
|
-
return null;
|
|
19086
|
-
}
|
|
19087
|
-
}
|
|
19088
|
-
async function writeCache3(models) {
|
|
19089
|
-
const path = catalogCachePath();
|
|
19090
|
-
try {
|
|
19091
|
-
await mkdir13(dirname16(path), { recursive: true });
|
|
19092
|
-
const cache = { version: CACHE_VERSION, fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), models };
|
|
19093
|
-
await writeFile12(path, JSON.stringify(cache, null, 2), "utf8");
|
|
19094
|
-
} catch {
|
|
19095
|
-
}
|
|
19096
|
-
}
|
|
19097
|
-
async function loadOpenRouterCatalog(opts2 = {}) {
|
|
19098
|
-
const ttlMs = opts2.ttlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
19099
|
-
const cached = await readCache3();
|
|
19100
|
-
const isFresh = cached && Date.now() - Date.parse(cached.fetchedAt) < ttlMs;
|
|
19101
|
-
if (isFresh) return cached.models;
|
|
19102
|
-
try {
|
|
19103
|
-
const models = await fetchOpenRouterCatalog(opts2.fetchImpl ?? fetch);
|
|
19104
|
-
await writeCache3(models);
|
|
19105
|
-
return models;
|
|
19106
|
-
} catch {
|
|
19107
|
-
return cached?.models ?? [];
|
|
19108
|
-
}
|
|
19109
|
-
}
|
|
19110
|
-
async function ensureOpenRouterCatalog(opts2 = {}) {
|
|
19111
|
-
try {
|
|
19112
|
-
const models = await loadOpenRouterCatalog(opts2);
|
|
19113
|
-
if (models.length > 0) registerOpenRouterModels(models);
|
|
19114
|
-
return models.length;
|
|
19115
|
-
} catch {
|
|
19116
|
-
return 0;
|
|
19117
|
-
}
|
|
19118
|
-
}
|
|
19119
|
-
var CACHE_VERSION, DEFAULT_CACHE_TTL_MS;
|
|
19120
|
-
var init_openrouter_catalog = __esm({
|
|
19121
|
-
"src/models/openrouter-catalog.ts"() {
|
|
19122
|
-
"use strict";
|
|
19123
|
-
init_registry();
|
|
19124
|
-
init_openrouter();
|
|
19125
|
-
CACHE_VERSION = 4;
|
|
19126
|
-
DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
19127
|
-
}
|
|
19128
|
-
});
|
|
19129
|
-
|
|
19130
20561
|
// src/models/requesty-catalog.ts
|
|
19131
20562
|
var requesty_catalog_exports = {};
|
|
19132
20563
|
__export(requesty_catalog_exports, {
|
|
@@ -19139,7 +20570,7 @@ __export(requesty_catalog_exports, {
|
|
|
19139
20570
|
});
|
|
19140
20571
|
import { readFile as readFile22, writeFile as writeFile13, mkdir as mkdir14 } from "fs/promises";
|
|
19141
20572
|
import { homedir as homedir23 } from "os";
|
|
19142
|
-
import { join as join32, dirname as
|
|
20573
|
+
import { join as join32, dirname as dirname18 } from "path";
|
|
19143
20574
|
function catalogCachePath2() {
|
|
19144
20575
|
const xdg = process.env.XDG_CONFIG_HOME || join32(homedir23(), ".config");
|
|
19145
20576
|
return join32(xdg, "kimiflare", "requesty-models.json");
|
|
@@ -19213,7 +20644,7 @@ async function readCache4() {
|
|
|
19213
20644
|
async function writeCache4(catalog) {
|
|
19214
20645
|
const path = catalogCachePath2();
|
|
19215
20646
|
try {
|
|
19216
|
-
await mkdir14(
|
|
20647
|
+
await mkdir14(dirname18(path), { recursive: true });
|
|
19217
20648
|
const cache = { version: CACHE_VERSION2, fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), ...catalog };
|
|
19218
20649
|
await writeFile13(path, JSON.stringify(cache, null, 2), "utf8");
|
|
19219
20650
|
} catch {
|
|
@@ -21261,7 +22692,7 @@ __export(app_helpers_exports, {
|
|
|
21261
22692
|
trackRecentFile: () => trackRecentFile
|
|
21262
22693
|
});
|
|
21263
22694
|
import { execSync as execSync3, spawn as spawn6 } from "child_process";
|
|
21264
|
-
import { existsSync as existsSync5, readFileSync as
|
|
22695
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, statSync as statSync5 } from "fs";
|
|
21265
22696
|
import { join as join34 } from "path";
|
|
21266
22697
|
import { platform as platform3 } from "os";
|
|
21267
22698
|
function replaceQueuedPrompt(queue2, key, full, display) {
|
|
@@ -21375,7 +22806,7 @@ function buildFilePickerIgnoreList(cwd) {
|
|
|
21375
22806
|
if (stats.size > MAX_GITIGNORE_SIZE) {
|
|
21376
22807
|
return hardcoded;
|
|
21377
22808
|
}
|
|
21378
|
-
const content =
|
|
22809
|
+
const content = readFileSync6(gitignorePath, "utf-8");
|
|
21379
22810
|
for (const line of content.split(/\r?\n/)) {
|
|
21380
22811
|
const trimmed = line.trim();
|
|
21381
22812
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -25833,6 +27264,8 @@ function humanizeToolTitle(toolName, originalTitle, tier) {
|
|
|
25833
27264
|
return humanizeMcpTitle(toolName, originalTitle, tier);
|
|
25834
27265
|
}
|
|
25835
27266
|
switch (toolName) {
|
|
27267
|
+
case "execute_code":
|
|
27268
|
+
return "Running code batch";
|
|
25836
27269
|
case "read":
|
|
25837
27270
|
return pick(tier, {
|
|
25838
27271
|
light: `Taking a quick look at ${extractPath(originalTitle)}`,
|
|
@@ -26523,6 +27956,7 @@ function isSettled(e) {
|
|
|
26523
27956
|
if (e.kind === "assistant") return !e.streaming;
|
|
26524
27957
|
if (e.kind === "tool") return e.status !== "running" && e.status !== "queued";
|
|
26525
27958
|
if (e.kind === "user") return !e.queued;
|
|
27959
|
+
if (e.kind === "info") return !e.pending;
|
|
26526
27960
|
return true;
|
|
26527
27961
|
}
|
|
26528
27962
|
function firstUnsettledIndex(events) {
|
|
@@ -29447,12 +30881,12 @@ auto (default) lets the coordinator assess substantial tasks and delegate indepe
|
|
|
29447
30881
|
setEvents((events) => [
|
|
29448
30882
|
...events,
|
|
29449
30883
|
{ kind: "user", key: mkKey2(), text: `/jev ${rest.join(" ")}` },
|
|
29450
|
-
{ kind: "info", key: eventKey, text: "Jev is evaluating\u2026" }
|
|
30884
|
+
{ kind: "info", key: eventKey, text: "Jev is evaluating\u2026", pending: true }
|
|
29451
30885
|
]);
|
|
29452
30886
|
try {
|
|
29453
30887
|
const context = noContext ? [] : await buildJevContext(question.prompt, ctx.messagesRef.current, process.cwd());
|
|
29454
30888
|
const receipt = formatJevContextReceipt(context, showContext);
|
|
29455
|
-
setResult(eventKey, { kind: "info", key: eventKey, text: `Jev is evaluating \xB7 ${receipt}
|
|
30889
|
+
setResult(eventKey, { kind: "info", key: eventKey, text: `Jev is evaluating \xB7 ${receipt}`, pending: true });
|
|
29456
30890
|
await new Promise((resolve15) => setTimeout(resolve15, 0));
|
|
29457
30891
|
const answer = await askJev(apiKey, question, { context });
|
|
29458
30892
|
const presentation = presentJevAnswer(question, answer);
|
|
@@ -30905,7 +32339,7 @@ __export(openrouter_oauth_exports, {
|
|
|
30905
32339
|
startLoopbackListener: () => startLoopbackListener
|
|
30906
32340
|
});
|
|
30907
32341
|
import { createHash as createHash4, randomBytes as randomBytes3 } from "crypto";
|
|
30908
|
-
import { createServer as
|
|
32342
|
+
import { createServer as createServer3 } from "http";
|
|
30909
32343
|
import { hostname } from "os";
|
|
30910
32344
|
function createPkce() {
|
|
30911
32345
|
const verifier = randomBytes3(32).toString("base64url");
|
|
@@ -30961,7 +32395,7 @@ async function startLoopbackListener(opts2 = {}) {
|
|
|
30961
32395
|
});
|
|
30962
32396
|
code.catch(() => void 0);
|
|
30963
32397
|
let settled = false;
|
|
30964
|
-
const server =
|
|
32398
|
+
const server = createServer3((req, res) => {
|
|
30965
32399
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
30966
32400
|
if (url.pathname !== "/callback") {
|
|
30967
32401
|
res.writeHead(404).end();
|
|
@@ -36285,7 +37719,7 @@ var init_run_compact = __esm({
|
|
|
36285
37719
|
|
|
36286
37720
|
// src/commands/save.ts
|
|
36287
37721
|
import { mkdir as mkdir16, writeFile as writeFile15, unlink as unlink5 } from "fs/promises";
|
|
36288
|
-
import { dirname as
|
|
37722
|
+
import { dirname as dirname20 } from "path";
|
|
36289
37723
|
async function saveCustomCommand(opts2) {
|
|
36290
37724
|
const dir = opts2.source === "project" ? projectCommandsDir(opts2.cwd) : globalCommandsDir();
|
|
36291
37725
|
const filepath = `${dir}/${opts2.name}.md`;
|
|
@@ -36296,7 +37730,7 @@ async function saveCustomCommand(opts2) {
|
|
|
36296
37730
|
if (opts2.effort) data.effort = opts2.effort;
|
|
36297
37731
|
const frontmatter = serializeFrontmatter(data);
|
|
36298
37732
|
const content = frontmatter + opts2.template;
|
|
36299
|
-
await mkdir16(
|
|
37733
|
+
await mkdir16(dirname20(filepath), { recursive: true });
|
|
36300
37734
|
await writeFile15(filepath, content, "utf8");
|
|
36301
37735
|
return { filepath };
|
|
36302
37736
|
}
|