autopilot-ai 1.21.0 → 1.22.0

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 CHANGED
@@ -18192,8 +18192,7 @@ var init_aster_workspaces = __esm({
18192
18192
  // src/server/aster-store.ts
18193
18193
  var aster_store_exports = {};
18194
18194
  __export(aster_store_exports, {
18195
- AsterStore: () => AsterStore,
18196
- defaultAsterDbPath: () => defaultAsterDbPath
18195
+ AsterStore: () => AsterStore
18197
18196
  });
18198
18197
  import Database4 from "better-sqlite3";
18199
18198
  import { createHash as createHash5, randomBytes as randomBytes2, randomUUID as randomUUID6 } from "crypto";
@@ -18207,22 +18206,6 @@ function defaultAsterDbPath() {
18207
18206
  function hashToken(token) {
18208
18207
  return createHash5("sha256").update(token).digest("hex");
18209
18208
  }
18210
- function cellMappingFromRow(row) {
18211
- return {
18212
- conversationId: row.conversation_id,
18213
- credentialId: row.credential_id,
18214
- workspaceId: row.workspace_id,
18215
- model: row.model,
18216
- sessionId: row.session_id,
18217
- cellName: row.cell_name,
18218
- cellId: row.cell_id ?? null,
18219
- status: row.status,
18220
- createAttempted: row.create_attempted === 1,
18221
- eventCursor: row.event_cursor,
18222
- createdAt: row.created_at,
18223
- updatedAt: row.updated_at
18224
- };
18225
- }
18226
18209
  function rowToConversation(row) {
18227
18210
  return {
18228
18211
  id: row.id,
@@ -18340,38 +18323,6 @@ var init_aster_store = __esm({
18340
18323
  created_at INTEGER NOT NULL,
18341
18324
  PRIMARY KEY (conversation_id, client_turn_id)
18342
18325
  );
18343
- CREATE TABLE IF NOT EXISTS aster_cell_mappings (
18344
- conversation_id TEXT PRIMARY KEY,
18345
- credential_id TEXT NOT NULL,
18346
- workspace_id TEXT NOT NULL,
18347
- model TEXT NOT NULL,
18348
- session_id TEXT NOT NULL UNIQUE,
18349
- cell_name TEXT NOT NULL UNIQUE,
18350
- cell_id TEXT,
18351
- create_key_hash TEXT NOT NULL,
18352
- create_fingerprint TEXT NOT NULL,
18353
- status TEXT NOT NULL,
18354
- create_attempted INTEGER NOT NULL DEFAULT 0,
18355
- event_cursor INTEGER NOT NULL DEFAULT 0,
18356
- created_at INTEGER NOT NULL,
18357
- updated_at INTEGER NOT NULL,
18358
- UNIQUE(credential_id, create_key_hash)
18359
- );
18360
- CREATE INDEX IF NOT EXISTS idx_aster_cells_lifecycle ON aster_cell_mappings(status, updated_at);
18361
- CREATE TABLE IF NOT EXISTS aster_github_grants (
18362
- conversation_id TEXT PRIMARY KEY,
18363
- token TEXT NOT NULL UNIQUE,
18364
- repository TEXT NOT NULL,
18365
- created_at INTEGER NOT NULL,
18366
- revoked_at INTEGER
18367
- );
18368
- CREATE TABLE IF NOT EXISTS aster_conversation_projects (
18369
- conversation_id TEXT PRIMARY KEY,
18370
- kind TEXT NOT NULL,
18371
- repository TEXT,
18372
- branch TEXT,
18373
- created_at INTEGER NOT NULL
18374
- );
18375
18326
  `);
18376
18327
  try {
18377
18328
  chmodSync2(dbPath, 384);
@@ -18381,49 +18332,6 @@ var init_aster_store = __esm({
18381
18332
  close() {
18382
18333
  this.db.close();
18383
18334
  }
18384
- reserveCellConversation(input) {
18385
- const keyHash = createHash5("sha256").update(input.idempotencyKey).digest("hex");
18386
- const fingerprint = createHash5("sha256").update(JSON.stringify([input.workspaceId, input.model])).digest("hex");
18387
- const reserve = this.db.transaction(() => {
18388
- const existing = this.db.prepare("SELECT * FROM aster_cell_mappings WHERE credential_id = ? AND create_key_hash = ?").get(input.credentialId, keyHash);
18389
- if (existing) {
18390
- const mapping = cellMappingFromRow(existing);
18391
- return mapping.workspaceId === input.workspaceId && mapping.model === input.model ? { kind: "replay", mapping } : { kind: "conflict" };
18392
- }
18393
- const now2 = Date.now();
18394
- this.db.prepare(`INSERT INTO aster_cell_mappings
18395
- (conversation_id, credential_id, workspace_id, model, session_id, cell_name, create_key_hash, create_fingerprint, status, created_at, updated_at)
18396
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'provisioning', ?, ?)`).run(input.conversationId, input.credentialId, input.workspaceId, input.model, input.sessionId, input.cellName, keyHash, fingerprint, now2, now2);
18397
- return { kind: "created", mapping: this.getCellMapping(input.conversationId) };
18398
- });
18399
- return reserve.immediate();
18400
- }
18401
- getCellMapping(conversationId) {
18402
- const row = this.db.prepare("SELECT * FROM aster_cell_mappings WHERE conversation_id = ?").get(conversationId);
18403
- return row ? cellMappingFromRow(row) : void 0;
18404
- }
18405
- listCellMappings() {
18406
- const rows = this.db.prepare("SELECT * FROM aster_cell_mappings ORDER BY created_at").all();
18407
- return rows.map(cellMappingFromRow);
18408
- }
18409
- getCredentialCellMapping(conversationId, credentialId) {
18410
- const row = this.db.prepare("SELECT * FROM aster_cell_mappings WHERE conversation_id = ? AND credential_id = ?").get(conversationId, credentialId);
18411
- return row ? cellMappingFromRow(row) : void 0;
18412
- }
18413
- markCellCreateAttempted(conversationId) {
18414
- this.db.prepare("UPDATE aster_cell_mappings SET create_attempted = 1, updated_at = ? WHERE conversation_id = ?").run(Date.now(), conversationId);
18415
- }
18416
- setCellId(conversationId, cellId) {
18417
- this.db.prepare("UPDATE aster_cell_mappings SET cell_id = ?, updated_at = ? WHERE conversation_id = ?").run(cellId, Date.now(), conversationId);
18418
- }
18419
- setCellStatus(conversationId, status) {
18420
- this.db.prepare("UPDATE aster_cell_mappings SET status = ?, updated_at = ? WHERE conversation_id = ?").run(status, Date.now(), conversationId);
18421
- }
18422
- advanceCellEventCursor(conversationId, cursor) {
18423
- if (!Number.isSafeInteger(cursor) || cursor < 0) throw new Error("invalid Hotcell event cursor");
18424
- const result2 = this.db.prepare("UPDATE aster_cell_mappings SET event_cursor = ?, updated_at = ? WHERE conversation_id = ? AND event_cursor < ?").run(cursor, Date.now(), conversationId, cursor);
18425
- return result2.changes === 1;
18426
- }
18427
18335
  createCredential(input) {
18428
18336
  const name = input.name.trim();
18429
18337
  if (!name || name.length > 100) throw new Error("credential name must be 1 to 100 characters");
@@ -18539,57 +18447,6 @@ var init_aster_store = __esm({
18539
18447
  });
18540
18448
  return update.immediate();
18541
18449
  }
18542
- setConversationStatus(conversationId, status, data = {}) {
18543
- const update = this.db.transaction(() => {
18544
- const now2 = Date.now();
18545
- const result2 = this.db.prepare("UPDATE aster_conversations SET status = ?, updated_at = ? WHERE id = ? AND active_run_id IS NULL").run(status, now2, conversationId);
18546
- if (result2.changes !== 1) return false;
18547
- this.insertEvent(conversationId, null, "status", { status, ...data }, now2);
18548
- return true;
18549
- });
18550
- return update.immediate();
18551
- }
18552
- /** Records a conversation's project once; the first value wins so retries can't change it. */
18553
- setConversationProject(conversationId, project) {
18554
- this.db.prepare(`INSERT OR IGNORE INTO aster_conversation_projects (conversation_id, kind, repository, branch, created_at)
18555
- VALUES (?, ?, ?, ?, ?)`).run(conversationId, project.kind, project.repository, project.branch, Date.now());
18556
- }
18557
- getConversationProject(conversationId) {
18558
- const row = this.db.prepare("SELECT kind, repository, branch FROM aster_conversation_projects WHERE conversation_id = ?").get(conversationId);
18559
- if (!row) return void 0;
18560
- return { kind: row.kind === "code" ? "code" : "chat", repository: row.repository, branch: row.branch };
18561
- }
18562
- /**
18563
- * The repository-scoped GitHub token for a code conversation, created once. It is stored
18564
- * as-is (like the server's own GitHub token, it never leaves the server except into that
18565
- * conversation's cell) so provisioning retries hand the cell the same value.
18566
- */
18567
- getOrCreateGitHubGrant(conversationId, repository) {
18568
- const existing = this.db.prepare("SELECT token FROM aster_github_grants WHERE conversation_id = ? AND revoked_at IS NULL").get(conversationId);
18569
- if (existing) return existing.token;
18570
- const token = "aghp_" + randomBytes2(32).toString("base64url");
18571
- this.db.prepare(`INSERT INTO aster_github_grants (conversation_id, token, repository, created_at) VALUES (?, ?, ?, ?)
18572
- 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());
18573
- return token;
18574
- }
18575
- resolveGitHubGrant(token) {
18576
- const row = this.db.prepare("SELECT conversation_id, repository FROM aster_github_grants WHERE token = ? AND revoked_at IS NULL").get(token);
18577
- return row ? { conversationId: row.conversation_id, repository: row.repository } : void 0;
18578
- }
18579
- revokeGitHubGrant(conversationId) {
18580
- this.db.prepare("UPDATE aster_github_grants SET revoked_at = ? WHERE conversation_id = ? AND revoked_at IS NULL").run(Date.now(), conversationId);
18581
- }
18582
- /** Switch an idle conversation's model; refused while a turn is active. */
18583
- setConversationModel(conversationId, model) {
18584
- const update = this.db.transaction(() => {
18585
- const now2 = Date.now();
18586
- const result2 = this.db.prepare("UPDATE aster_conversations SET model = ?, updated_at = ? WHERE id = ? AND active_run_id IS NULL").run(model, now2, conversationId);
18587
- if (result2.changes !== 1) return false;
18588
- this.insertEvent(conversationId, null, "model.changed", { model }, now2);
18589
- return true;
18590
- });
18591
- return update.immediate();
18592
- }
18593
18450
  appendEvent(conversationId, runId, type, data) {
18594
18451
  const append = this.db.transaction(() => this.insertEvent(conversationId, runId, type, data, Date.now()));
18595
18452
  return append.immediate();
@@ -19492,103 +19349,6 @@ var init_wake_scheduler = __esm({
19492
19349
  }
19493
19350
  });
19494
19351
 
19495
- // src/server/aster-github.ts
19496
- function mapRepository(raw) {
19497
- return {
19498
- fullName: raw.full_name,
19499
- name: raw.name,
19500
- owner: raw.owner?.login ?? raw.full_name.split("/")[0] ?? "",
19501
- private: raw.private,
19502
- description: raw.description,
19503
- defaultBranch: raw.default_branch ?? "main",
19504
- updatedAt: raw.pushed_at ?? raw.updated_at ?? null
19505
- };
19506
- }
19507
- var AsterGitHubError, REPOSITORY_FULL_NAME_RE, REPOSITORY_NAME_RE, API, AsterGitHub;
19508
- var init_aster_github = __esm({
19509
- "src/server/aster-github.ts"() {
19510
- "use strict";
19511
- AsterGitHubError = class extends Error {
19512
- constructor(message2, status, code) {
19513
- super(message2);
19514
- this.status = status;
19515
- this.code = code;
19516
- this.name = "AsterGitHubError";
19517
- }
19518
- status;
19519
- code;
19520
- };
19521
- REPOSITORY_FULL_NAME_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]{1,100}$/;
19522
- REPOSITORY_NAME_RE = /^[A-Za-z0-9._-]{1,100}$/;
19523
- API = "https://api.github.com";
19524
- AsterGitHub = class {
19525
- constructor(token = process.env.GITHUB_TOKEN, fetchImpl2 = fetch) {
19526
- this.token = token;
19527
- this.fetchImpl = fetchImpl2;
19528
- }
19529
- token;
19530
- fetchImpl;
19531
- get configured() {
19532
- return Boolean(this.token?.trim());
19533
- }
19534
- /** Repositories the token can push to, most recently pushed first. */
19535
- async listRepositories(query = "") {
19536
- const raw = await this.request(
19537
- "GET",
19538
- "/user/repos?per_page=100&sort=pushed&affiliation=owner,collaborator,organization_member"
19539
- );
19540
- const needle = query.trim().toLowerCase();
19541
- return raw.map(mapRepository).filter((repo) => !needle || repo.fullName.toLowerCase().includes(needle) || (repo.description ?? "").toLowerCase().includes(needle));
19542
- }
19543
- async getRepository(fullName) {
19544
- if (!REPOSITORY_FULL_NAME_RE.test(fullName)) throw new AsterGitHubError("Invalid repository name", 400, "invalid_repository");
19545
- return mapRepository(await this.request("GET", `/repos/${fullName}`));
19546
- }
19547
- /** Creates a repository under the token's user, initialized so it has a default branch to clone. */
19548
- async createRepository(input) {
19549
- if (!REPOSITORY_NAME_RE.test(input.name) || input.name === "." || input.name === "..") {
19550
- throw new AsterGitHubError("Repository names may use letters, digits, '.', '_' and '-'", 400, "invalid_repository_name");
19551
- }
19552
- const description = input.description?.trim().slice(0, 350);
19553
- return mapRepository(await this.request("POST", "/user/repos", {
19554
- name: input.name,
19555
- private: input.private,
19556
- auto_init: true,
19557
- ...description ? { description } : {}
19558
- }));
19559
- }
19560
- async request(method, path, body) {
19561
- if (!this.configured) throw new AsterGitHubError("GitHub is not connected on the server", 503, "github_not_configured");
19562
- let response;
19563
- try {
19564
- response = await this.fetchImpl(API + path, {
19565
- method,
19566
- headers: {
19567
- Accept: "application/vnd.github+json",
19568
- Authorization: `Bearer ${this.token}`,
19569
- "X-GitHub-Api-Version": "2022-11-28",
19570
- "User-Agent": "autopilot-aster",
19571
- ...body === void 0 ? {} : { "Content-Type": "application/json" }
19572
- },
19573
- body: body === void 0 ? void 0 : JSON.stringify(body),
19574
- signal: AbortSignal.timeout(2e4)
19575
- });
19576
- } catch {
19577
- throw new AsterGitHubError("GitHub could not be reached", 502, "github_unreachable");
19578
- }
19579
- if (response.ok) return await response.json();
19580
- const detail = await response.json().catch(() => ({}));
19581
- const message2 = detail.errors?.[0]?.message ?? detail.message ?? `GitHub returned HTTP ${response.status}`;
19582
- if (response.status === 404) throw new AsterGitHubError("Repository not found or not accessible with the server's GitHub token", 404, "repository_not_found");
19583
- if (response.status === 401) throw new AsterGitHubError("The server's GitHub token was rejected", 502, "github_token_rejected");
19584
- if (response.status === 403) throw new AsterGitHubError(`GitHub refused the request: ${message2}`, 403, "github_forbidden");
19585
- if (response.status === 422) throw new AsterGitHubError(message2, 422, "github_validation_failed");
19586
- throw new AsterGitHubError(message2, 502, "github_error");
19587
- }
19588
- };
19589
- }
19590
- });
19591
-
19592
19352
  // src/server/aster-tools.ts
19593
19353
  import { realpath as realpath5 } from "fs/promises";
19594
19354
  import { isAbsolute as isAbsolute6, relative as relative9, sep as sep5 } from "path";
@@ -19629,31 +19389,6 @@ async function assertAsterWorkspacePath(workspaceRoot, value, allowMissing = fal
19629
19389
  assertNotSensitive(root, actual);
19630
19390
  return candidate;
19631
19391
  }
19632
- function assertAsterCellPath(value) {
19633
- if (typeof value !== "string" || !value.trim()) throw new Error("workspace_path_invalid");
19634
- const root = "/workspace";
19635
- const candidate = value.startsWith("/") ? posixNormalize(value) : posixNormalize(`${root}/${value}`);
19636
- if (candidate !== root && !candidate.startsWith(root + "/")) throw new Error("workspace_path_forbidden");
19637
- const parts = candidate.slice(root.length).split("/").filter(Boolean).map((part) => part.toLowerCase());
19638
- if (parts.some((part) => part === ".env" || part.startsWith(".env.") || SENSITIVE_NAMES.has(part))) {
19639
- throw new Error("workspace_sensitive_path_forbidden");
19640
- }
19641
- return candidate;
19642
- }
19643
- function posixNormalize(value) {
19644
- const parts = value.split("/");
19645
- const out = [];
19646
- for (const part of parts) {
19647
- if (!part || part === ".") continue;
19648
- if (part === "..") {
19649
- if (out.length === 0) return "/__escape__";
19650
- out.pop();
19651
- continue;
19652
- }
19653
- out.push(part);
19654
- }
19655
- return "/" + out.join("/");
19656
- }
19657
19392
  function isLikelySkKey(candidate) {
19658
19393
  if (/^sk-or-v1-[A-Za-z0-9]{16,}/.test(candidate)) return true;
19659
19394
  return candidate.slice(3).split(/[-_]/).some((chunk) => chunk.length >= 20 && /\d/.test(chunk));
@@ -19704,517 +19439,8 @@ var init_aster_tools = __esm({
19704
19439
  }
19705
19440
  });
19706
19441
 
19707
- // src/server/aster-memory.ts
19708
- import Database5 from "better-sqlite3";
19709
- import { randomUUID as randomUUID7 } from "crypto";
19710
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync10 } from "fs";
19711
- import { dirname as dirname20 } from "path";
19712
- function rowToItem(row) {
19713
- return {
19714
- id: row.id,
19715
- kind: row.kind,
19716
- key: row.key,
19717
- title: row.title,
19718
- content: row.content,
19719
- pinned: row.pinned === 1,
19720
- sourceConversationId: row.source_conversation_id,
19721
- createdAt: row.created_at,
19722
- updatedAt: row.updated_at
19723
- };
19724
- }
19725
- function normalizeKey(kind, value) {
19726
- const raw = value.toLowerCase().startsWith(`${kind}:`) ? value.slice(kind.length + 1) : value;
19727
- const slug = raw.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
19728
- const key = `${kind}:${slug}`;
19729
- return KEY_RE.test(key) ? key : null;
19730
- }
19731
- function formatCard(card) {
19732
- return `- ${card.title} (${card.kind}): ${card.content}`;
19733
- }
19734
- function mentions(text, card) {
19735
- const haystack = ` ${text.toLowerCase()} `;
19736
- const words = /* @__PURE__ */ new Set();
19737
- for (const part of [card.title, card.key?.split(":")[1]?.replace(/-/g, " ") ?? ""]) {
19738
- for (const word of part.toLowerCase().split(/[^\p{L}\p{N}]+/u)) {
19739
- if (word.length >= 3 && !STOP_WORDS.has(word)) words.add(word);
19740
- }
19741
- }
19742
- return [...words].some((word) => new RegExp(`[^\\p{L}\\p{N}]${escapeRegExp(word)}[^\\p{L}\\p{N}]`, "u").test(haystack));
19743
- }
19744
- function escapeRegExp(value) {
19745
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19746
- }
19747
- 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;
19748
- var init_aster_memory = __esm({
19749
- "src/server/aster-memory.ts"() {
19750
- "use strict";
19751
- init_openrouter();
19752
- init_aster_store();
19753
- init_aster_tools();
19754
- MAX_CARD_CHARS = 900;
19755
- MAX_FACT_CHARS = 300;
19756
- MAX_MEMORY_PROMPT_ITEMS = 150;
19757
- BRIEF_FACTS = 15;
19758
- BRIEF_RECENT_SESSIONS = 6;
19759
- BRIEF_RECENT_CARDS = 3;
19760
- KEY_RE = /^(person|project):[a-z0-9][a-z0-9-]{0,80}$/;
19761
- AsterMemory = class {
19762
- constructor(dbPath = defaultAsterDbPath(), options = {}) {
19763
- this.options = options;
19764
- mkdirSync10(dirname20(dbPath), { recursive: true, mode: 448 });
19765
- this.db = new Database5(dbPath);
19766
- this.db.pragma("journal_mode = WAL");
19767
- this.db.pragma("busy_timeout = 5000");
19768
- this.db.exec(`
19769
- CREATE TABLE IF NOT EXISTS aster_memories (
19770
- id TEXT PRIMARY KEY,
19771
- kind TEXT NOT NULL,
19772
- key TEXT,
19773
- title TEXT NOT NULL,
19774
- content TEXT NOT NULL,
19775
- pinned INTEGER NOT NULL DEFAULT 0,
19776
- source_conversation_id TEXT,
19777
- created_at INTEGER NOT NULL,
19778
- updated_at INTEGER NOT NULL,
19779
- deleted INTEGER NOT NULL DEFAULT 0
19780
- );
19781
- CREATE UNIQUE INDEX IF NOT EXISTS idx_aster_memories_key ON aster_memories(key) WHERE key IS NOT NULL AND deleted = 0;
19782
- CREATE INDEX IF NOT EXISTS idx_aster_memories_kind ON aster_memories(kind, deleted, updated_at);
19783
- `);
19784
- try {
19785
- chmodSync3(dbPath, 384);
19786
- } catch {
19787
- }
19788
- }
19789
- options;
19790
- db;
19791
- /** Card keys already shown to the agent per conversation, so later turns only add new ones. */
19792
- shown = /* @__PURE__ */ new Map();
19793
- get enabled() {
19794
- return this.options.enabled !== false && Boolean(this.apiKey);
19795
- }
19796
- get apiKey() {
19797
- return this.options.apiKey ?? process.env.OPENROUTER_API_KEY;
19798
- }
19799
- get model() {
19800
- return this.options.model ?? process.env.AUTOPILOT_ASTER_MEMORY_MODEL ?? "google/gemini-3.8-flash";
19801
- }
19802
- close() {
19803
- this.db.close();
19804
- }
19805
- list(kind) {
19806
- 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();
19807
- return rows.map(rowToItem);
19808
- }
19809
- get(id) {
19810
- const row = this.db.prepare("SELECT * FROM aster_memories WHERE id = ? AND deleted = 0").get(id);
19811
- return row ? rowToItem(row) : void 0;
19812
- }
19813
- /** Soft delete; the user removed it, so extraction must not silently recreate it from this item. */
19814
- delete(id) {
19815
- return this.db.prepare("UPDATE aster_memories SET deleted = 1, updated_at = ? WHERE id = ? AND deleted = 0").run(Date.now(), id).changes === 1;
19816
- }
19817
- /** Write or merge one item. person/project/session are keyed; facts are by id. */
19818
- upsert(input) {
19819
- const now2 = Date.now();
19820
- const limit = input.kind === "fact" ? MAX_FACT_CHARS : MAX_CARD_CHARS;
19821
- const title = input.title.trim().slice(0, 120);
19822
- const content = input.content.trim().slice(0, limit);
19823
- 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;
19824
- if (existing) {
19825
- 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);
19826
- return { item: this.get(existing.id), change: "updated" };
19827
- }
19828
- const id = randomUUID7();
19829
- this.db.prepare(`INSERT INTO aster_memories (id, kind, key, title, content, pinned, source_conversation_id, created_at, updated_at)
19830
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.kind, input.key ?? null, title, content, input.pinned ? 1 : 0, input.sourceConversationId ?? null, now2, now2);
19831
- return { item: this.get(id), change: "added" };
19832
- }
19833
- /**
19834
- * The memory note for a turn: a full brief on a conversation's first turn, afterwards only
19835
- * person/project cards the user's message newly brings up. Empty when there is nothing new.
19836
- */
19837
- brief(conversationId, userText) {
19838
- const items = this.list();
19839
- if (items.length === 0) return "";
19840
- const shown = this.shown.get(conversationId);
19841
- const cards = items.filter((item) => item.kind === "person" || item.kind === "project");
19842
- const mentioned = cards.filter((card) => mentions(userText, card));
19843
- if (shown) {
19844
- const fresh = mentioned.filter((card) => !shown.has(card.id));
19845
- if (fresh.length === 0) return "";
19846
- fresh.forEach((card) => shown.add(card.id));
19847
- return `[Aster memory \u2014 relevant to this message]
19848
- ${fresh.map(formatCard).join("\n")}`;
19849
- }
19850
- const shownNow = /* @__PURE__ */ new Set();
19851
- 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);
19852
- const featured = [...mentioned, ...cards.filter((card) => !mentioned.includes(card)).slice(0, BRIEF_RECENT_CARDS)];
19853
- featured.forEach((card) => shownNow.add(card.id));
19854
- const others = cards.filter((card) => !shownNow.has(card.id));
19855
- const sessions = items.filter((item) => item.kind === "session" && item.key !== conversationId).slice(0, BRIEF_RECENT_SESSIONS);
19856
- this.shown.set(conversationId, shownNow);
19857
- const sections = [
19858
- "[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.]"
19859
- ];
19860
- if (facts.length) sections.push("About the user:\n" + facts.map((fact) => `- ${fact.content}`).join("\n"));
19861
- if (featured.length) sections.push("People and projects:\n" + featured.map(formatCard).join("\n"));
19862
- if (others.length) sections.push("Also known (details in memory.md): " + others.map((card) => card.title).join(", "));
19863
- if (sessions.length) sections.push("Recent chats:\n" + sessions.map((session) => `- ${session.title}: ${session.content}`).join("\n"));
19864
- return sections.join("\n\n");
19865
- }
19866
- /** All memory as Markdown, written into the cell so the agent can search it on demand. */
19867
- snapshot() {
19868
- const items = this.list();
19869
- const group = (kind, heading, render2) => {
19870
- const entries = items.filter((item) => item.kind === kind);
19871
- return entries.length ? `## ${heading}
19872
-
19873
- ${entries.map(render2).join("\n")}
19874
- ` : "";
19875
- };
19876
- return [
19877
- "# Aster memory\n\nWhat the user's assistant remembers across chats. Read-only here; it is updated automatically after each turn.\n",
19878
- group("fact", "About the user", (item) => `- ${item.content}`),
19879
- group("person", "People", (item) => `### ${item.title}
19880
- ${item.content}
19881
- `),
19882
- group("project", "Projects", (item) => `### ${item.title}
19883
- ${item.content}
19884
- `),
19885
- group("session", "Chats", (item) => `- **${item.title}** (${new Date(item.updatedAt).toISOString().slice(0, 10)}): ${item.content}`)
19886
- ].filter(Boolean).join("\n");
19887
- }
19888
- /** Read one exchange and update memory. Returns what changed (for the app's "memory updated" chip). */
19889
- async remember(exchange) {
19890
- if (!this.enabled) return [];
19891
- const result2 = await this.extract(exchange);
19892
- if (!result2) return [];
19893
- const changes = [];
19894
- const known = new Map(this.list().filter((item) => item.kind !== "session").map((item) => [item.id, item]));
19895
- for (const id of result2.deletes ?? []) {
19896
- const item = known.get(id);
19897
- if (item && !item.pinned && this.delete(id)) changes.push({ id, kind: item.kind, title: item.title, change: "removed" });
19898
- }
19899
- for (const upsert of result2.upserts ?? []) {
19900
- const kind = upsert.kind === "person" || upsert.kind === "project" || upsert.kind === "fact" ? upsert.kind : void 0;
19901
- const title = upsert.title?.trim();
19902
- const content = upsert.content?.trim();
19903
- if (!kind || !title || !content || containsLikelyProviderSecret(content)) continue;
19904
- const key = kind === "fact" ? null : normalizeKey(kind, upsert.key ?? title);
19905
- if (kind !== "fact" && !key) continue;
19906
- const id = kind === "fact" && upsert.id && known.has(upsert.id) ? upsert.id : null;
19907
- const { item, change } = this.upsert({ kind, key, id, title, content, pinned: upsert.pinned === true, sourceConversationId: exchange.conversationId });
19908
- changes.push({ id: item.id, kind, title: item.title, change });
19909
- }
19910
- const session = result2.session;
19911
- if (session?.title?.trim() && session.summary?.trim()) {
19912
- const repo = exchange.project?.kind === "code" && exchange.project.repository ? ` [${exchange.project.repository}]` : "";
19913
- this.upsert({
19914
- kind: "session",
19915
- key: exchange.conversationId,
19916
- title: session.title.trim().slice(0, 80) + repo,
19917
- content: session.summary.trim(),
19918
- sourceConversationId: exchange.conversationId
19919
- });
19920
- }
19921
- return changes;
19922
- }
19923
- async extract(exchange) {
19924
- 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);
19925
- const currentSession = this.list("session").find((item) => item.key === exchange.conversationId);
19926
- 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)";
19927
- const context = exchange.project?.kind === "code" && exchange.project.repository ? `Code chat in GitHub repository ${exchange.project.repository}.` : "General chat.";
19928
- const user = [
19929
- `CURRENT MEMORY (one JSON object per line):
19930
- ${memory}`,
19931
- `THIS CHAT: ${context}${currentSession ? ` Summary so far: ${currentSession.content}` : ""}`,
19932
- `LATEST USER MESSAGE:
19933
- ${exchange.userText.slice(0, 6e3)}`,
19934
- `ASSISTANT REPLY:
19935
- ${exchange.assistantText.slice(0, 4e3)}`
19936
- ].join("\n\n");
19937
- let response;
19938
- try {
19939
- response = await (this.options.fetchImpl ?? fetch)(openRouterUrl("chat/completions"), {
19940
- method: "POST",
19941
- headers: { ...openRouterHeaders(this.apiKey), "Content-Type": "application/json" },
19942
- body: JSON.stringify({
19943
- model: this.model,
19944
- temperature: 0,
19945
- response_format: { type: "json_object" },
19946
- messages: [{ role: "system", content: EXTRACTION_PROMPT }, { role: "user", content: user }]
19947
- }),
19948
- signal: AbortSignal.timeout(3e4)
19949
- });
19950
- } catch {
19951
- return void 0;
19952
- }
19953
- if (!response.ok) return void 0;
19954
- const body = await response.json().catch(() => void 0);
19955
- const text = body?.choices?.[0]?.message?.content;
19956
- if (!text) return void 0;
19957
- try {
19958
- return JSON.parse(text.replace(/^```(?:json)?\s*|\s*```$/g, ""));
19959
- } catch {
19960
- return void 0;
19961
- }
19962
- }
19963
- };
19964
- 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.
19965
-
19966
- Remember what will matter in future, unrelated chats:
19967
- - 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.
19968
- - PROJECTS: name, what it is, client or owner, goals, scope, stack, repository, status, key decisions, and ideas the user brainstormed or chose.
19969
- - FACTS about the user: durable preferences, circumstances (job, freelance work, location if stated), ways of working, tools they use.
19970
-
19971
- 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.
19972
-
19973
- Rules:
19974
- - If the user explicitly asks to remember something, you MUST store it, with "pinned": true.
19975
- - 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).
19976
- - Keys: "person:<first-last>" or "project:<short-name>", lowercase with hyphens.
19977
- - 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".
19978
- - Most exchanges contain nothing worth remembering: return empty "upserts" then. Never invent details.
19979
- - 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.
19980
-
19981
- Return only JSON:
19982
- {"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":"..."}}`;
19983
- STOP_WORDS = /* @__PURE__ */ new Set(["the", "and", "for", "app", "project", "new", "with", "website", "site"]);
19984
- }
19985
- });
19986
-
19987
- // src/server/aster-github-proxy.ts
19988
- import { createServer } from "http";
19989
- import { request as httpsRequest } from "https";
19990
- import { request as httpRequest } from "http";
19991
- function extractToken(req) {
19992
- const header = req.headers.authorization;
19993
- if (!header) return void 0;
19994
- const [scheme, value = ""] = header.split(" ", 2);
19995
- if (/^(bearer|token)$/i.test(scheme ?? "")) return value.trim() || void 0;
19996
- if (/^basic$/i.test(scheme ?? "")) {
19997
- const decoded = Buffer.from(value, "base64").toString("utf8");
19998
- const password = decoded.slice(decoded.indexOf(":") + 1);
19999
- return password || void 0;
20000
- }
20001
- return void 0;
20002
- }
20003
- function deny(res, status, message2) {
20004
- res.writeHead(status, { "content-type": "application/json" });
20005
- res.end(JSON.stringify({ message: message2 }));
20006
- }
20007
- var GIT_PATH_RE, API_PATH_RE, FORWARDED_REQUEST_HEADERS, HOP_BY_HOP, AsterGitHubProxy;
20008
- var init_aster_github_proxy = __esm({
20009
- "src/server/aster-github-proxy.ts"() {
20010
- "use strict";
20011
- 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)$/;
20012
- API_PATH_RE = /^\/api\/repos\/([A-Za-z0-9-]{1,39})\/([A-Za-z0-9._-]{1,100})(\/.*)?$/;
20013
- FORWARDED_REQUEST_HEADERS = ["accept", "content-type", "content-encoding", "content-length", "git-protocol", "user-agent", "x-github-api-version"];
20014
- HOP_BY_HOP = /* @__PURE__ */ new Set(["connection", "keep-alive", "transfer-encoding", "upgrade", "proxy-authenticate", "proxy-authorization", "te", "trailer"]);
20015
- AsterGitHubProxy = class {
20016
- constructor(options) {
20017
- this.options = options;
20018
- this.gitUpstream = new URL(options.gitUpstream ?? "https://github.com");
20019
- this.apiUpstream = new URL(options.apiUpstream ?? "https://api.github.com");
20020
- }
20021
- options;
20022
- server;
20023
- gitUpstream;
20024
- apiUpstream;
20025
- async listen(port, host) {
20026
- this.server = createServer((req, res) => this.handle(req, res));
20027
- await new Promise((resolve19, reject) => {
20028
- this.server.once("error", reject);
20029
- this.server.listen(port, host, () => resolve19());
20030
- });
20031
- const address = this.server.address();
20032
- return typeof address === "object" && address ? address.port : port;
20033
- }
20034
- close() {
20035
- this.server?.close();
20036
- this.server = void 0;
20037
- }
20038
- handle(req, res) {
20039
- const url = new URL(req.url ?? "/", "http://proxy.local");
20040
- const git4 = url.pathname.match(GIT_PATH_RE);
20041
- const api = git4 ? null : url.pathname.match(API_PATH_RE);
20042
- if (!git4 && !api) return deny(res, 404, "Only this conversation's repository is reachable");
20043
- const grantToken = extractToken(req);
20044
- if (!grantToken) {
20045
- res.writeHead(401, { "www-authenticate": 'Basic realm="aster-github"', "content-type": "text/plain" });
20046
- res.end("credentials required");
20047
- return;
20048
- }
20049
- const grant = this.options.resolveGrant(grantToken);
20050
- if (!grant) return deny(res, 403, "This repository grant is invalid or was revoked");
20051
- const [, owner, name] = git4 ?? api;
20052
- if (`${owner}/${name}`.toLowerCase() !== grant.repository.toLowerCase()) {
20053
- return deny(res, 403, `This conversation can only access ${grant.repository}`);
20054
- }
20055
- const upstream = git4 ? new URL(`/${owner}/${name}.git/${git4[3]}${url.search}`, this.gitUpstream) : new URL(`/repos/${owner}/${name}${api[3] ?? ""}${url.search}`, this.apiUpstream);
20056
- const headers = {};
20057
- for (const name2 of FORWARDED_REQUEST_HEADERS) {
20058
- const value = req.headers[name2];
20059
- if (typeof value === "string") headers[name2] = value;
20060
- }
20061
- headers["user-agent"] ??= "autopilot-aster";
20062
- headers.authorization = git4 ? "Basic " + Buffer.from(`x-access-token:${this.options.token}`).toString("base64") : `Bearer ${this.options.token}`;
20063
- const send = upstream.protocol === "https:" ? httpsRequest : httpRequest;
20064
- const forward = send(upstream, { method: req.method, headers }, (upstreamRes) => {
20065
- const responseHeaders = {};
20066
- for (const [key, value] of Object.entries(upstreamRes.headers)) {
20067
- if (value !== void 0 && !HOP_BY_HOP.has(key)) responseHeaders[key] = value;
20068
- }
20069
- res.writeHead(upstreamRes.statusCode ?? 502, responseHeaders);
20070
- upstreamRes.pipe(res);
20071
- });
20072
- forward.on("error", () => {
20073
- if (!res.headersSent) deny(res, 502, "GitHub could not be reached");
20074
- else res.destroy();
20075
- });
20076
- req.pipe(forward);
20077
- }
20078
- };
20079
- }
20080
- });
20081
-
20082
- // src/server/aster-push.ts
20083
- import Database6 from "better-sqlite3";
20084
- import { createSign } from "crypto";
20085
- import { readFileSync as readFileSync6 } from "fs";
20086
- import { connect } from "http2";
20087
- function truncate2(value, limit) {
20088
- const clean = value.replace(/\s+/g, " ").trim();
20089
- return clean.length > limit ? clean.slice(0, limit - 1) + "\u2026" : clean;
20090
- }
20091
- var DEVICE_TOKEN_RE, AsterPush;
20092
- var init_aster_push = __esm({
20093
- "src/server/aster-push.ts"() {
20094
- "use strict";
20095
- init_aster_store();
20096
- DEVICE_TOKEN_RE = /^[0-9a-f]{64,200}$/i;
20097
- AsterPush = class {
20098
- db;
20099
- options;
20100
- privateKey;
20101
- jwt;
20102
- sessions = /* @__PURE__ */ new Map();
20103
- constructor(dbPath = defaultAsterDbPath(), options = {}, env2 = process.env) {
20104
- this.options = {
20105
- ...options,
20106
- keyPath: options.keyPath ?? env2.AUTOPILOT_ASTER_APNS_KEY_PATH,
20107
- keyId: options.keyId ?? env2.AUTOPILOT_ASTER_APNS_KEY_ID ?? "",
20108
- teamId: options.teamId ?? env2.AUTOPILOT_ASTER_APNS_TEAM_ID ?? "",
20109
- topic: options.topic ?? env2.AUTOPILOT_ASTER_APNS_TOPIC ?? "com.aster.ai"
20110
- };
20111
- this.db = new Database6(dbPath);
20112
- this.db.pragma("journal_mode = WAL");
20113
- this.db.pragma("busy_timeout = 5000");
20114
- this.db.exec(`
20115
- CREATE TABLE IF NOT EXISTS aster_devices (
20116
- token TEXT PRIMARY KEY,
20117
- environment TEXT NOT NULL,
20118
- created_at INTEGER NOT NULL,
20119
- updated_at INTEGER NOT NULL
20120
- );
20121
- `);
20122
- }
20123
- /** Whether an APNs key is configured; devices are still recorded without one. */
20124
- get configured() {
20125
- return Boolean((this.options.keyPath || this.options.deliver) && this.options.keyId && this.options.teamId);
20126
- }
20127
- register(token, environment) {
20128
- if (!DEVICE_TOKEN_RE.test(token)) return false;
20129
- const now2 = Date.now();
20130
- this.db.prepare(`INSERT INTO aster_devices (token, environment, created_at, updated_at) VALUES (?, ?, ?, ?)
20131
- ON CONFLICT(token) DO UPDATE SET environment = excluded.environment, updated_at = excluded.updated_at`).run(token.toLowerCase(), environment, now2, now2);
20132
- return true;
20133
- }
20134
- unregister(token) {
20135
- this.db.prepare("DELETE FROM aster_devices WHERE token = ?").run(token.toLowerCase());
20136
- }
20137
- deviceCount() {
20138
- return this.db.prepare("SELECT COUNT(*) AS n FROM aster_devices").get().n;
20139
- }
20140
- /** Sends to every registered device; tokens Apple reports as gone are removed. Never throws. */
20141
- async notify(message2) {
20142
- if (!this.configured) return;
20143
- const devices = this.db.prepare("SELECT token, environment FROM aster_devices").all();
20144
- if (devices.length === 0) return;
20145
- const payload = JSON.stringify({
20146
- aps: {
20147
- alert: { title: truncate2(message2.title, 80), body: truncate2(message2.body, 180) },
20148
- sound: "default",
20149
- "thread-id": message2.conversationId,
20150
- ...message2.category ? { category: message2.category } : {}
20151
- },
20152
- conversationId: message2.conversationId,
20153
- ...message2.approvalId ? { approvalId: message2.approvalId } : {}
20154
- });
20155
- await Promise.all(devices.map(async (device) => {
20156
- const host = device.environment === "development" ? "api.sandbox.push.apple.com" : "api.push.apple.com";
20157
- try {
20158
- const status = await this.send(host, device.token, payload);
20159
- if (status === 410 || status === 400) this.unregister(device.token);
20160
- } catch {
20161
- }
20162
- }));
20163
- }
20164
- close() {
20165
- for (const session of this.sessions.values()) session.close();
20166
- this.sessions.clear();
20167
- this.db.close();
20168
- }
20169
- async send(host, token, payload) {
20170
- const headers = {
20171
- authorization: `bearer ${this.token()}`,
20172
- "apns-topic": this.options.topic,
20173
- "apns-push-type": "alert",
20174
- "apns-priority": "10"
20175
- };
20176
- if (this.options.deliver) return this.options.deliver({ host, token, headers, payload });
20177
- const session = this.session(host);
20178
- return await new Promise((resolve19, reject) => {
20179
- const stream = session.request({ ":method": "POST", ":path": `/3/device/${token}`, ...headers, "content-type": "application/json" });
20180
- let status = 0;
20181
- stream.setTimeout(15e3, () => stream.close());
20182
- stream.on("response", (responseHeaders) => {
20183
- status = Number(responseHeaders[":status"]) || 0;
20184
- });
20185
- stream.on("error", reject);
20186
- stream.on("close", () => resolve19(status));
20187
- stream.resume();
20188
- stream.end(payload);
20189
- });
20190
- }
20191
- session(host) {
20192
- const existing = this.sessions.get(host);
20193
- if (existing && !existing.closed && !existing.destroyed) return existing;
20194
- const session = connect(`https://${host}`);
20195
- session.on("error", () => this.sessions.delete(host));
20196
- session.on("close", () => this.sessions.delete(host));
20197
- session.unref();
20198
- this.sessions.set(host, session);
20199
- return session;
20200
- }
20201
- /** Provider token (ES256 JWT); Apple accepts one for up to an hour, so it is reused for 50 minutes. */
20202
- token() {
20203
- const now2 = Math.floor(Date.now() / 1e3);
20204
- if (this.jwt && now2 - this.jwt.issuedAt < 50 * 60) return this.jwt.value;
20205
- this.privateKey ??= readFileSync6(this.options.keyPath, "utf8");
20206
- const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
20207
- const unsigned = `${encode({ alg: "ES256", kid: this.options.keyId })}.${encode({ iss: this.options.teamId, iat: now2 })}`;
20208
- const signature = createSign("SHA256").update(unsigned).sign({ key: this.privateKey, dsaEncoding: "ieee-p1363" }).toString("base64url");
20209
- this.jwt = { value: `${unsigned}.${signature}`, issuedAt: now2 };
20210
- return this.jwt.value;
20211
- }
20212
- };
20213
- }
20214
- });
20215
-
20216
19442
  // src/server/aster-api.ts
20217
- import { randomUUID as randomUUID8 } from "crypto";
19443
+ import { randomUUID as randomUUID7 } from "crypto";
20218
19444
  import { unlink as unlink2 } from "fs/promises";
20219
19445
  import { resolve as resolve13 } from "path";
20220
19446
  function parseCursor(header, query) {
@@ -20223,23 +19449,8 @@ function parseCursor(header, query) {
20223
19449
  const cursor = Number(value);
20224
19450
  return Number.isSafeInteger(cursor) ? cursor : void 0;
20225
19451
  }
20226
- function collectAssistantText(store, conversationId, runId, after) {
20227
- let text = "";
20228
- let cursor = after;
20229
- for (; ; ) {
20230
- const events = store.eventsAfter(conversationId, cursor, 500);
20231
- if (events.length === 0) break;
20232
- for (const event of events) {
20233
- if (event.runId === runId && event.type === "assistant.delta" && typeof event.data.text === "string") text += event.data.text;
20234
- }
20235
- cursor = events.at(-1).sequence;
20236
- if (text.length > 2e4) break;
20237
- }
20238
- return text;
20239
- }
20240
- function publicConversation(conversation, project) {
19452
+ function publicConversation(conversation) {
20241
19453
  return {
20242
- ...project ? { kind: project.kind, repository: project.repository, branch: project.branch } : {},
20243
19454
  conversationId: conversation.id,
20244
19455
  workspaceId: conversation.workspaceId,
20245
19456
  model: conversation.model,
@@ -20263,25 +19474,6 @@ function publicApproval(approval) {
20263
19474
  decision: approval.decision
20264
19475
  };
20265
19476
  }
20266
- async function selectableModels(config2) {
20267
- if (config2.modelCatalog === "fixed") return config2.models.map((id) => ({ id }));
20268
- const catalog = await loadOpenRouterCatalog();
20269
- return catalog.filter((model) => model.supports.tools).map((model) => ({
20270
- id: model.id,
20271
- ...model.name ? { name: model.name } : {},
20272
- contextWindow: model.contextWindow,
20273
- inputPerMtok: model.pricing.inputPerMtok,
20274
- outputPerMtok: model.pricing.outputPerMtok,
20275
- ...model.created !== void 0 ? { created: model.created } : {},
20276
- reasoning: model.supports.reasoning,
20277
- vision: model.supports.vision === true
20278
- }));
20279
- }
20280
- async function isSelectableModel(config2, model) {
20281
- if (!model.trim() || model.length > 200) return false;
20282
- if (config2.modelCatalog === "fixed") return config2.models.includes(model);
20283
- return (await selectableModels(config2)).some((entry) => entry.id === model);
20284
- }
20285
19477
  async function readJsonBody(req) {
20286
19478
  const chunks = [];
20287
19479
  let bytes = 0;
@@ -20313,27 +19505,23 @@ function json(res, status, data) {
20313
19505
  res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
20314
19506
  res.end(JSON.stringify(data));
20315
19507
  }
20316
- var API_PREFIX, MAX_BODY_BYTES, MAX_USER_TURN_CHARS, CLIENT_TURN_ID_RE, CREATE_IDEMPOTENCY_KEY_RE, MAX_APPROVAL_ARG_BYTES, CONVERSATION_ID_RE, ALLOWED_ASTER_TOOLS, AsterApi;
19508
+ var API_PREFIX, MAX_BODY_BYTES, MAX_USER_TURN_CHARS, CLIENT_TURN_ID_RE, MAX_APPROVAL_ARG_BYTES, CONVERSATION_ID_RE, ALLOWED_ASTER_TOOLS, AsterApi;
20317
19509
  var init_aster_api = __esm({
20318
19510
  "src/server/aster-api.ts"() {
20319
19511
  "use strict";
19512
+ init_system_prompt();
20320
19513
  init_executor();
20321
19514
  init_store();
19515
+ init_worktrees();
20322
19516
  init_sessions();
20323
19517
  init_version();
20324
19518
  init_aster_store();
20325
19519
  init_aster_workspaces();
20326
- init_openrouter_catalog();
20327
- init_aster_github();
20328
- init_aster_memory();
20329
- init_aster_github_proxy();
20330
- init_aster_push();
20331
19520
  init_aster_tools();
20332
19521
  API_PREFIX = "/api/v1";
20333
- MAX_BODY_BYTES = 4 * 1024 * 1024;
20334
- MAX_USER_TURN_CHARS = 5e5;
19522
+ MAX_BODY_BYTES = 64 * 1024;
19523
+ MAX_USER_TURN_CHARS = 2e4;
20335
19524
  CLIENT_TURN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
20336
- CREATE_IDEMPOTENCY_KEY_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
20337
19525
  MAX_APPROVAL_ARG_BYTES = 24 * 1024;
20338
19526
  CONVERSATION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
20339
19527
  ALLOWED_ASTER_TOOLS = ["read", "write", "edit"];
@@ -20341,62 +19529,11 @@ var init_aster_api = __esm({
20341
19529
  store;
20342
19530
  recovery;
20343
19531
  waiters = /* @__PURE__ */ new Map();
20344
- connectedEventStreams = /* @__PURE__ */ new Map();
20345
19532
  runtime;
20346
19533
  config;
20347
- github;
20348
- memory;
20349
- constructor(config2, runtime, github = new AsterGitHub(), memory, push) {
19534
+ constructor(config2, runtime) {
20350
19535
  this.config = config2;
20351
19536
  this.runtime = runtime;
20352
- this.github = github;
20353
- this.memory = memory;
20354
- this.push = push;
20355
- }
20356
- githubProxy;
20357
- /** Where cells reach the repository-scoped GitHub proxy, when it is running. */
20358
- githubProxyUrl;
20359
- /**
20360
- * Starts the repository-scoped GitHub proxy when configured
20361
- * (AUTOPILOT_ASTER_GITHUB_PROXY_LISTEN=host:port, AUTOPILOT_ASTER_GITHUB_PROXY_URL as cells see it).
20362
- */
20363
- async startGitHubProxy(env2 = process.env) {
20364
- const listen = env2.AUTOPILOT_ASTER_GITHUB_PROXY_LISTEN;
20365
- const publicUrl = env2.AUTOPILOT_ASTER_GITHUB_PROXY_URL;
20366
- const token = env2.GITHUB_TOKEN;
20367
- if (!listen || !publicUrl || !token || this.githubProxy) return;
20368
- const separator = listen.lastIndexOf(":");
20369
- const proxy = new AsterGitHubProxy({ token, resolveGrant: (grant) => this.getStore().resolveGitHubGrant(grant) });
20370
- await proxy.listen(Number(listen.slice(separator + 1)), listen.slice(0, separator));
20371
- this.githubProxy = proxy;
20372
- this.githubProxyUrl = publicUrl.replace(/\/+$/, "");
20373
- }
20374
- push;
20375
- getPush() {
20376
- try {
20377
- return this.push ??= new AsterPush();
20378
- } catch {
20379
- return void 0;
20380
- }
20381
- }
20382
- /** "Your task finished" (or failed) on the user's devices; cancelled turns stay silent. */
20383
- notifyTurnEnd(store, conversationId, runId, status, reason, userText, after) {
20384
- if (status !== "completed" && status !== "failed") return;
20385
- if ((this.connectedEventStreams.get(conversationId) ?? 0) > 0) return;
20386
- const push = this.getPush();
20387
- if (!push?.configured) return;
20388
- const reply = status === "completed" ? collectAssistantText(store, conversationId, runId, after).trim() : "";
20389
- const title = userText.trim().split("\n")[0] || "Aster";
20390
- const body = status === "completed" ? reply ? `\u2705 ${reply}` : "\u2705 Done" : `\u26A0\uFE0F Couldn\u2019t finish${reason && reason !== "error" ? `: ${reason}` : ""}`;
20391
- void push.notify({ title, body, conversationId });
20392
- }
20393
- getMemory() {
20394
- if (process.env.AUTOPILOT_ASTER_MEMORY === "off") return void 0;
20395
- try {
20396
- return this.memory ??= new AsterMemory();
20397
- } catch {
20398
- return void 0;
20399
- }
20400
19537
  }
20401
19538
  async handle(req, res) {
20402
19539
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
@@ -20418,7 +19555,7 @@ var init_aster_api = __esm({
20418
19555
  service: "autopilot",
20419
19556
  apiVersion: "v1",
20420
19557
  serviceVersion: getAppVersion(),
20421
- 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"] : []]
19558
+ capabilities: ["workspaces", "conversations", "turns", "event-replay", "cancellation", "human-approvals"]
20422
19559
  });
20423
19560
  }
20424
19561
  if (path === `${API_PREFIX}/workspaces` && method === "GET") {
@@ -20429,86 +19566,23 @@ var init_aster_api = __esm({
20429
19566
  }
20430
19567
  if (path === `${API_PREFIX}/models` && method === "GET") {
20431
19568
  if (!this.hasScope(principal, "models:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks model/read scope");
20432
- const models = await selectableModels(config2);
20433
- if (models.length === 0) return sendError(res, 503, "model_catalog_unavailable", "The model catalog is unavailable; try again shortly");
20434
- return json(res, 200, { models: models.map((model) => model.id), catalog: models });
19569
+ return json(res, 200, { models: config2.models });
20435
19570
  }
20436
19571
  if (path === `${API_PREFIX}/conversations` && method === "POST") {
20437
19572
  if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
20438
19573
  const body = await readJsonBody(req);
20439
- if (Object.keys(body).some((key) => !["workspaceId", "model", "kind", "repository"].includes(key))) {
20440
- return sendError(res, 400, "unsupported_field", "Conversation creation accepts workspaceId, model, kind, and repository; paths are server-configured");
20441
- }
20442
- if (body.kind !== void 0 && body.kind !== "chat" && body.kind !== "code") {
20443
- return sendError(res, 400, "invalid_kind", 'kind must be "chat" or "code"');
20444
- }
20445
- if (body.kind === "code" && (typeof body.repository !== "string" || !REPOSITORY_FULL_NAME_RE.test(body.repository))) {
20446
- return sendError(res, 400, "repository_required", "Code conversations need a repository as owner/name");
20447
- }
20448
- if (body.kind !== "code" && body.repository !== void 0) {
20449
- return sendError(res, 400, "unsupported_field", "repository is only accepted for code conversations");
19574
+ if (Object.keys(body).some((key) => key !== "workspaceId" && key !== "model")) {
19575
+ return sendError(res, 400, "unsupported_field", "Conversation creation accepts only workspaceId and model; paths are server-configured");
20450
19576
  }
20451
19577
  if (typeof body.workspaceId !== "string" || !principal.workspaceIds.includes(body.workspaceId)) {
20452
19578
  return sendError(res, 403, "workspace_forbidden", "Workspace ID is not in this credential's scope");
20453
19579
  }
20454
19580
  const workspace = findAsterWorkspace(config2, body.workspaceId);
20455
19581
  if (!workspace) return sendError(res, 404, "workspace_not_found", "Workspace is not configured");
20456
- if (typeof body.model !== "string" || !await isSelectableModel(config2, body.model)) {
19582
+ if (typeof body.model !== "string" || !config2.models.includes(body.model)) {
20457
19583
  return sendError(res, 400, "model_not_allowed", "Select a model from the configured model list");
20458
19584
  }
20459
- const idempotencyKey = req.headers["idempotency-key"];
20460
- if (typeof idempotencyKey !== "string" || !CREATE_IDEMPOTENCY_KEY_RE.test(idempotencyKey)) {
20461
- return sendError(res, 400, "idempotency_key_required", "Conversation creation requires an Idempotency-Key header of 1-128 safe characters");
20462
- }
20463
- const kind = body.kind;
20464
- return await this.createConversation(res, store, principal, workspace, body.model, idempotencyKey, kind, body.repository);
20465
- }
20466
- if (path === `${API_PREFIX}/devices` && method === "POST") {
20467
- if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
20468
- const body = await readJsonBody(req);
20469
- const environment = body.environment === "development" ? "development" : "production";
20470
- const push = this.getPush();
20471
- if (!push || typeof body.token !== "string" || !push.register(body.token, environment)) {
20472
- return sendError(res, 400, "invalid_device_token", "A hex APNs device token is required");
20473
- }
20474
- return json(res, 200, { registered: true, notifications: push.configured });
20475
- }
20476
- const deviceMatch = path.match(/^\/api\/v1\/devices\/([0-9a-fA-F]{64,200})$/);
20477
- if (deviceMatch && method === "DELETE") {
20478
- if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
20479
- this.getPush()?.unregister(deviceMatch[1]);
20480
- return json(res, 200, { unregistered: true });
20481
- }
20482
- if (path === `${API_PREFIX}/memory` && method === "GET") {
20483
- if (!this.hasScope(principal, "conversations:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/read scope");
20484
- const memory = this.getMemory();
20485
- return json(res, 200, { enabled: Boolean(memory?.enabled), items: memory ? memory.list() : [] });
20486
- }
20487
- const memoryMatch = path.match(/^\/api\/v1\/memory\/([^/]+)$/);
20488
- if (memoryMatch && method === "DELETE") {
20489
- if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
20490
- const memory = this.getMemory();
20491
- if (!memory || !memory.delete(memoryMatch[1])) return sendError(res, 404, "memory_not_found", "Memory item not found");
20492
- return json(res, 200, { deleted: memoryMatch[1] });
20493
- }
20494
- if (path === `${API_PREFIX}/github/repositories` && (method === "GET" || method === "POST")) {
20495
- if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
20496
- try {
20497
- if (method === "GET") {
20498
- return json(res, 200, { repositories: await this.github.listRepositories(url.searchParams.get("q") ?? "") });
20499
- }
20500
- const body = await readJsonBody(req);
20501
- if (typeof body.name !== "string") return sendError(res, 400, "invalid_repository_name", "name is required");
20502
- const repository = await this.github.createRepository({
20503
- name: body.name.trim(),
20504
- private: body.private !== false,
20505
- ...typeof body.description === "string" ? { description: body.description } : {}
20506
- });
20507
- return json(res, 201, { repository });
20508
- } catch (error) {
20509
- if (error instanceof AsterGitHubError) return sendError(res, error.status, error.code, error.message);
20510
- throw error;
20511
- }
19585
+ return await this.createConversation(res, store, workspace, body.model);
20512
19586
  }
20513
19587
  const eventsMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/events$/);
20514
19588
  if (eventsMatch && method === "GET") {
@@ -20525,7 +19599,7 @@ var init_aster_api = __esm({
20525
19599
  const conversation = await this.authorizedConversation(store, principal, turnMatch[1]);
20526
19600
  if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
20527
19601
  const body = await readJsonBody(req);
20528
- 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");
19602
+ if (Object.keys(body).some((key) => key !== "text" && key !== "clientTurnId")) return sendError(res, 400, "unsupported_field", "Turns accept text and an optional clientTurnId; workspace and model are fixed by the conversation");
20529
19603
  const hasClientTurnId = Object.hasOwn(body, "clientTurnId");
20530
19604
  const clientTurnId = hasClientTurnId && typeof body.clientTurnId === "string" ? body.clientTurnId : void 0;
20531
19605
  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");
@@ -20533,18 +19607,7 @@ var init_aster_api = __esm({
20533
19607
  const text = requestText.trim();
20534
19608
  if (!text || text.length > MAX_USER_TURN_CHARS) return sendError(res, 400, "invalid_turn", `Turn text must be 1-${MAX_USER_TURN_CHARS} characters`);
20535
19609
  if (containsLikelyProviderSecret(text)) return sendError(res, 400, "secret_input_rejected", "Provider credentials must not be sent in conversation text");
20536
- let target = conversation;
20537
- if (Object.hasOwn(body, "model")) {
20538
- if (typeof body.model !== "string" || !await isSelectableModel(config2, body.model)) {
20539
- return sendError(res, 400, "model_not_allowed", "Select a model from the model catalog");
20540
- }
20541
- if (body.model !== conversation.model) {
20542
- const switched = await this.switchModel(res, store, conversation, body.model);
20543
- if (!switched) return;
20544
- target = switched;
20545
- }
20546
- }
20547
- return await this.appendTurn(res, store, target, clientTurnId, requestText, text);
19610
+ return await this.appendTurn(res, store, conversation, clientTurnId, requestText, text);
20548
19611
  }
20549
19612
  const cancelMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/cancel$/);
20550
19613
  if (cancelMatch && method === "POST") {
@@ -20553,80 +19616,12 @@ var init_aster_api = __esm({
20553
19616
  if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
20554
19617
  return this.cancelConversation(res, store, conversation);
20555
19618
  }
20556
- const lifecycleMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/(pause|resume)$/);
20557
- if (lifecycleMatch && method === "POST") {
20558
- if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
20559
- const conversation = await this.authorizedConversation(store, principal, lifecycleMatch[1]);
20560
- if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
20561
- return lifecycleMatch[2] === "pause" ? this.pauseConversation(res, store, conversation) : this.resumeConversation(res, store, conversation);
20562
- }
20563
- const artifactsMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/artifacts(\/file)?$/);
20564
- if (artifactsMatch && method === "GET") {
20565
- if (!this.hasScope(principal, "conversations:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/read scope");
20566
- const conversation = await this.authorizedConversation(store, principal, artifactsMatch[1]);
20567
- if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
20568
- const cell = this.readyCell(res, store, conversation.id);
20569
- if (!cell) return;
20570
- if (!this.runtime.listArtifacts || !this.runtime.readArtifact) return sendError(res, 503, "artifacts_unavailable", "Artifacts are not configured");
20571
- if (!artifactsMatch[2]) return json(res, 200, { artifacts: await this.runtime.listArtifacts(cell) });
20572
- try {
20573
- const artifact = await this.runtime.readArtifact(cell, url.searchParams.get("path") ?? "");
20574
- res.writeHead(200, {
20575
- "content-type": artifact.mimeType,
20576
- "content-length": artifact.data.length,
20577
- "content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(artifact.name)}`,
20578
- "cache-control": "no-store",
20579
- "x-content-type-options": "nosniff"
20580
- });
20581
- res.end(artifact.data);
20582
- return;
20583
- } catch (error) {
20584
- const code = error instanceof Error ? error.message : "";
20585
- if (code === "invalid_artifact_path") return sendError(res, 400, code, "Artifact path is invalid");
20586
- if (code === "artifact_too_large") return sendError(res, 413, code, "Artifact is larger than 50 MB");
20587
- return sendError(res, 404, "artifact_not_found", "Artifact not found");
20588
- }
20589
- }
20590
- const checkpointsMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/checkpoints$/);
20591
- if (checkpointsMatch && method === "GET") {
20592
- if (!this.hasScope(principal, "conversations:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/read scope");
20593
- const conversation = await this.authorizedConversation(store, principal, checkpointsMatch[1]);
20594
- if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
20595
- const cell = this.readyCell(res, store, conversation.id);
20596
- if (!cell) return;
20597
- if (!this.runtime.listCheckpoints) return sendError(res, 503, "checkpoints_unavailable", "Checkpoints are not configured");
20598
- return json(res, 200, { checkpoints: await this.runtime.listCheckpoints(cell) });
20599
- }
20600
- const restoreMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/checkpoints\/([0-9a-f]{7,40})\/restore$/);
20601
- if (restoreMatch && method === "POST") {
20602
- if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
20603
- const conversation = await this.authorizedConversation(store, principal, restoreMatch[1]);
20604
- if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
20605
- if (conversation.activeRunId) return sendError(res, 409, "turn_active", "Wait for the current turn to finish or cancel it before restoring");
20606
- const cell = this.readyCell(res, store, conversation.id);
20607
- if (!cell) return;
20608
- if (!this.runtime.restoreCheckpoint) return sendError(res, 503, "checkpoints_unavailable", "Checkpoints are not configured");
20609
- try {
20610
- const checkpoint = await this.runtime.restoreCheckpoint(cell, conversation.id, restoreMatch[2]);
20611
- store.appendEvent(conversation.id, null, "checkpoint.restored", { restoredFrom: restoreMatch[2], ...checkpoint });
20612
- return json(res, 200, { checkpoint });
20613
- } catch (error) {
20614
- if (error instanceof Error && error.message === "checkpoint_not_found") return sendError(res, 404, "checkpoint_not_found", "Checkpoint not found");
20615
- return sendError(res, 500, "checkpoint_restore_failed", "The workspace could not be restored");
20616
- }
20617
- }
20618
19619
  const conversationMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)$/);
20619
- if (conversationMatch && method === "DELETE") {
20620
- if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
20621
- const conversation = await this.authorizedConversation(store, principal, conversationMatch[1]);
20622
- if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
20623
- return this.destroyConversation(res, store, conversation);
20624
- }
20625
19620
  if (conversationMatch && method === "GET") {
20626
19621
  if (!this.hasScope(principal, "conversations:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/read scope");
20627
19622
  const conversation = await this.authorizedConversation(store, principal, conversationMatch[1]);
20628
19623
  if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
20629
- return json(res, 200, publicConversation(conversation, store.getConversationProject(conversation.id)));
19624
+ return json(res, 200, publicConversation(conversation));
20630
19625
  }
20631
19626
  const approvalMatch = path.match(/^\/api\/v1\/approvals\/([^/]+)$/);
20632
19627
  if (approvalMatch && method === "GET") {
@@ -20634,8 +19629,7 @@ var init_aster_api = __esm({
20634
19629
  const approval = store.getApproval(approvalMatch[1]);
20635
19630
  if (!approval) return sendError(res, 404, "approval_not_found", "Approval not found");
20636
19631
  const conversation = store.getConversation(approval.conversationId);
20637
- const mapping = store.getCellMapping(approval.conversationId);
20638
- if (!conversation || mapping?.credentialId !== principal.credentialId || !principal.workspaceIds.includes(conversation.workspaceId)) return sendError(res, 404, "approval_not_found", "Approval not found");
19632
+ if (!conversation || !principal.workspaceIds.includes(conversation.workspaceId)) return sendError(res, 404, "approval_not_found", "Approval not found");
20639
19633
  return json(res, 200, publicApproval(approval));
20640
19634
  }
20641
19635
  if (approvalMatch && method === "POST") {
@@ -20643,8 +19637,7 @@ var init_aster_api = __esm({
20643
19637
  const approval = store.getApproval(approvalMatch[1]);
20644
19638
  if (!approval) return sendError(res, 404, "approval_not_found", "Approval not found");
20645
19639
  const conversation = store.getConversation(approval.conversationId);
20646
- const mapping = store.getCellMapping(approval.conversationId);
20647
- if (!conversation || mapping?.credentialId !== principal.credentialId || !principal.workspaceIds.includes(conversation.workspaceId)) return sendError(res, 404, "approval_not_found", "Approval not found");
19640
+ if (!conversation || !principal.workspaceIds.includes(conversation.workspaceId)) return sendError(res, 404, "approval_not_found", "Approval not found");
20648
19641
  const body = await readJsonBody(req);
20649
19642
  if (Object.keys(body).some((key) => key !== "decision")) return sendError(res, 400, "unsupported_field", "Approval resolution accepts decision only");
20650
19643
  if (body.decision !== "allow" && body.decision !== "deny") return sendError(res, 400, "invalid_approval_decision", "decision must be allow or deny");
@@ -20665,12 +19658,6 @@ var init_aster_api = __esm({
20665
19658
  }
20666
19659
  this.store?.close();
20667
19660
  this.store = void 0;
20668
- this.memory?.close();
20669
- this.memory = void 0;
20670
- this.githubProxy?.close();
20671
- this.githubProxy = void 0;
20672
- this.push?.close();
20673
- this.push = void 0;
20674
19661
  }
20675
19662
  getStore() {
20676
19663
  return this.store ??= new AsterStore();
@@ -20681,8 +19668,7 @@ var init_aster_api = __esm({
20681
19668
  async authorizedConversation(store, principal, id) {
20682
19669
  if (!CONVERSATION_ID_RE.test(id)) return void 0;
20683
19670
  const conversation = store.getConversation(id);
20684
- const mapping = store.getCellMapping(id);
20685
- if (!conversation || mapping?.credentialId !== principal.credentialId || !principal.workspaceIds.includes(conversation.workspaceId)) return void 0;
19671
+ if (!conversation || !principal.workspaceIds.includes(conversation.workspaceId)) return void 0;
20686
19672
  const activeRunId = conversation.activeRunId;
20687
19673
  if (activeRunId) {
20688
19674
  const runs = new RunStore();
@@ -20722,14 +19708,6 @@ var init_aster_api = __esm({
20722
19708
  } else if (run.status === "needs_input") {
20723
19709
  runs.transition(run.id, "cancelled", "approval_expired_on_restart");
20724
19710
  store.updateConversationStatus(conversation.id, run.id, "interrupted", { reason: "approval_expired_on_restart" });
20725
- } else {
20726
- const mapping = store.getCellMapping(conversation.id);
20727
- if (mapping?.cellId && this.runtime.resumeTurn) {
20728
- this.runtime.resumeTurn(this.cellTurn(store, conversation, run.id, mapping.cellId, mapping.eventCursor, run.task ?? ""));
20729
- } else {
20730
- runs.transition(run.id, "failed", "server_restarted");
20731
- store.updateConversationStatus(conversation.id, run.id, "interrupted", { reason: "server_restart_recovery" });
20732
- }
20733
19711
  }
20734
19712
  }
20735
19713
  } finally {
@@ -20738,149 +19716,50 @@ var init_aster_api = __esm({
20738
19716
  })();
20739
19717
  return this.recovery;
20740
19718
  }
20741
- /** A turn handle for re-attaching to a cell turn after a restart (no prompt is sent). */
20742
- cellTurn(store, conversation, runId, cellId, cursor, userText) {
20743
- const publishEvent = (type, data) => {
20744
- store.appendEvent(conversation.id, runId, type, data);
20745
- };
20746
- return {
20747
- runId,
20748
- conversation,
20749
- cellId,
20750
- cellEventCursor: cursor,
20751
- userText,
20752
- sessionFile: {},
20753
- messages: [],
20754
- executor: new ToolExecutor([]),
20755
- allowedTools: /* @__PURE__ */ new Set(),
20756
- maxToolIterations: null,
20757
- maxRuntimeMs: null,
20758
- askPermission: async () => "deny",
20759
- publishEvent,
20760
- onCellCursor: (next) => store.advanceCellEventCursor(conversation.id, next),
20761
- finish: (status, reason) => {
20762
- const runs = new RunStore();
20763
- try {
20764
- const run = runs.getRun(runId);
20765
- if (run && ["queued", "running", "waiting", "needs_input"].includes(run.status)) runs.transition(runId, status, reason ?? status);
20766
- } catch {
20767
- } finally {
20768
- runs.close();
20769
- }
20770
- const updated = store.updateConversationStatus(conversation.id, runId, status, reason ? { reason } : {});
20771
- if (updated) store.appendEvent(conversation.id, runId, status, reason ? { reason } : {});
20772
- if (updated) this.notifyTurnEnd(store, conversation.id, runId, status, reason, userText, 0);
20773
- }
20774
- };
20775
- }
20776
- async createConversation(res, store, principal, workspace, model, idempotencyKey, kind, repository) {
20777
- const candidateId = randomUUID8();
20778
- const candidateSessionId = randomUUID8();
20779
- const reservation = store.reserveCellConversation({
20780
- conversationId: candidateId,
20781
- credentialId: principal.credentialId,
20782
- workspaceId: workspace.id,
20783
- model,
20784
- sessionId: candidateSessionId,
20785
- cellName: `aster-${candidateId}`,
20786
- idempotencyKey
20787
- });
20788
- if (reservation.kind === "conflict") return sendError(res, 409, "idempotency_conflict", "Idempotency-Key was already used for different conversation parameters");
20789
- const mapping = reservation.mapping;
20790
- if (mapping.status === "destroyed") return sendError(res, 410, "conversation_destroyed", "Conversation was already destroyed");
20791
- if (mapping.status === "ready") {
20792
- const existing = store.getConversation(mapping.conversationId);
20793
- if (existing) return json(res, 200, publicConversation(existing, store.getConversationProject(existing.id)));
20794
- }
20795
- if (!this.runtime.provisionConversation) {
20796
- store.setCellStatus(mapping.conversationId, "failed");
20797
- return sendError(res, 503, "hotcell_unavailable", "Hotcell conversation runtime is not configured");
20798
- }
20799
- let project;
20800
- if (kind) {
20801
- let stored = store.getConversationProject(mapping.conversationId);
20802
- if (!stored) {
20803
- if (kind === "code") {
20804
- let repo;
20805
- try {
20806
- repo = await this.github.getRepository(repository);
20807
- } catch (error) {
20808
- if (error instanceof AsterGitHubError) return sendError(res, error.status, error.code, error.message);
20809
- throw error;
20810
- }
20811
- store.setConversationProject(mapping.conversationId, {
20812
- kind,
20813
- repository: repo.fullName,
20814
- branch: `aster/${mapping.conversationId.slice(0, 8)}`
20815
- });
20816
- stored = { kind, repository: repo.fullName, branch: `aster/${mapping.conversationId.slice(0, 8)}` };
20817
- project = { kind, repository: repo.fullName, baseBranch: repo.defaultBranch, branch: stored.branch };
20818
- } else {
20819
- store.setConversationProject(mapping.conversationId, { kind, repository: null, branch: null });
20820
- stored = { kind, repository: null, branch: null };
20821
- }
20822
- }
20823
- if (!project) {
20824
- 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" };
20825
- }
20826
- if (project.kind === "code" && project.repository && this.githubProxyUrl) {
20827
- project.github = {
20828
- token: store.getOrCreateGitHubGrant(mapping.conversationId, project.repository),
20829
- gitUrl: `${this.githubProxyUrl}/git`,
20830
- apiUrl: `${this.githubProxyUrl}/api`
20831
- };
20832
- }
19719
+ async createConversation(res, store, workspace, model) {
19720
+ const id = randomUUID7();
19721
+ const sessionId = randomUUID7();
19722
+ const worktreeManager = new RunWorktreeManager();
19723
+ let worktree;
19724
+ try {
19725
+ worktree = await worktreeManager.create(id, workspace.rootPath);
19726
+ } catch {
19727
+ sendError(res, 409, "workspace_repository_unavailable", "Configured workspace must be an accessible Git repository");
19728
+ return;
20833
19729
  }
19730
+ const tools = createAsterTools(worktree.cwd);
19731
+ const messages = [
19732
+ { role: "system", content: buildSystemPrompt({ cwd: worktree.cwd, tools, model, preferPullRequests: true }) },
19733
+ { role: "system", content: "You are operating through Aster. Only read, write, and edit tools are available. All file access is confined to the selected workspace. Never request, reveal, store, or repeat provider credentials or secrets." }
19734
+ ];
19735
+ const sessionFile = {
19736
+ id: sessionId,
19737
+ cwd: worktree.cwd,
19738
+ model,
19739
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
19740
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
19741
+ messages,
19742
+ title: "Aster conversation"
19743
+ };
20834
19744
  try {
20835
- const allowCreate = !mapping.createAttempted;
20836
- if (allowCreate) store.markCellCreateAttempted(mapping.conversationId);
20837
- const provisioned = await this.runtime.provisionConversation({
20838
- conversationId: mapping.conversationId,
20839
- sessionId: mapping.sessionId,
20840
- cellName: mapping.cellName,
20841
- workspaceId: workspace.id,
20842
- workspaceRoot: workspace.rootPath,
20843
- model,
20844
- allowCreate,
20845
- onCellCreated: (cellId) => store.setCellId(mapping.conversationId, cellId),
20846
- ...project ? { project } : {}
20847
- });
20848
- store.setCellId(mapping.conversationId, provisioned.cellId);
20849
- const conversation = store.getConversation(mapping.conversationId) ?? store.createConversation({
20850
- id: mapping.conversationId,
19745
+ await saveSession(sessionFile);
19746
+ const conversation = store.createConversation({
19747
+ id,
20851
19748
  workspaceId: workspace.id,
20852
19749
  model,
20853
- sessionId: mapping.sessionId,
20854
- worktreePath: "/workspace",
20855
- cwd: "/workspace",
20856
- branch: "cell"
19750
+ sessionId,
19751
+ worktreePath: worktree.worktreePath,
19752
+ cwd: worktree.cwd,
19753
+ branch: worktree.branch
20857
19754
  });
20858
- const sessionFile = {
20859
- id: mapping.sessionId,
20860
- cwd: "/workspace",
20861
- model,
20862
- createdAt: new Date(mapping.createdAt).toISOString(),
20863
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
20864
- messages: [],
20865
- title: "Aster conversation"
20866
- };
20867
- await saveSession(sessionFile);
20868
- store.setCellStatus(mapping.conversationId, "ready");
20869
- store.appendEvent(mapping.conversationId, null, "conversation.created", { conversationId: mapping.conversationId, workspaceId: workspace.id, model });
20870
- json(res, reservation.kind === "created" ? 201 : 200, publicConversation(conversation, store.getConversationProject(conversation.id)));
19755
+ store.appendEvent(id, null, "conversation.created", { conversationId: id, workspaceId: workspace.id, model });
19756
+ json(res, 201, publicConversation(conversation));
20871
19757
  } catch {
20872
- const current = store.getCellMapping(mapping.conversationId);
20873
- if (current?.cellId && this.runtime.destroyCell) {
20874
- try {
20875
- await this.runtime.destroyCell(current.cellId);
20876
- store.setCellStatus(mapping.conversationId, "failed");
20877
- } catch {
20878
- store.setCellStatus(mapping.conversationId, "cleanup_pending");
20879
- }
20880
- } else {
20881
- store.setCellStatus(mapping.conversationId, "failed");
20882
- }
20883
- sendError(res, 503, "conversation_provisioning_failed", "Conversation provisioning failed; retry with the same Idempotency-Key to reconcile its state");
19758
+ await unlink2(resolve13(sessionsDir(), `${sessionId}.json`)).catch(() => {
19759
+ });
19760
+ await worktreeManager.discard(worktree).catch(() => {
19761
+ });
19762
+ sendError(res, 500, "conversation_create_failed", "Conversation could not be created");
20884
19763
  }
20885
19764
  }
20886
19765
  async appendTurn(res, store, conversation, clientTurnId, requestText, text) {
@@ -20896,16 +19775,7 @@ var init_aster_api = __esm({
20896
19775
  return;
20897
19776
  }
20898
19777
  }
20899
- const cellMapping = store.getCellMapping(conversation.id);
20900
- if (cellMapping?.status === "paused") {
20901
- sendError(res, 409, "conversation_paused", "Conversation is paused; resume it before submitting a turn");
20902
- return;
20903
- }
20904
- if (!cellMapping?.cellId || ["provisioning", "destroying", "cleanup_pending", "destroyed", "failed"].includes(cellMapping.status)) {
20905
- sendError(res, 409, "conversation_cell_unavailable", "Conversation cell is not available");
20906
- return;
20907
- }
20908
- const runId = randomUUID8();
19778
+ const runId = randomUUID7();
20909
19779
  if (!idempotent && !store.beginTurn(conversation.id, runId)) {
20910
19780
  sendError(res, 409, "conversation_busy", "A turn is already active for this conversation");
20911
19781
  return;
@@ -20982,13 +19852,9 @@ var init_aster_api = __esm({
20982
19852
  const publishEvent = (type, data) => {
20983
19853
  store.appendEvent(conversation.id, runId, type, data);
20984
19854
  };
20985
- const turnStartSequence = store.getConversation(conversation.id)?.lastEventSequence ?? 0;
20986
19855
  const active = {
20987
19856
  runId,
20988
19857
  conversation,
20989
- cellId: cellMapping.cellId,
20990
- cellEventCursor: cellMapping.eventCursor,
20991
- userText: text,
20992
19858
  sessionFile,
20993
19859
  messages: sessionFile.messages,
20994
19860
  executor,
@@ -20997,24 +19863,11 @@ var init_aster_api = __esm({
20997
19863
  maxRuntimeMs: run.maxRuntimeMs,
20998
19864
  askPermission: (request) => this.askForApproval(store, conversation, runId, request, publishEvent),
20999
19865
  publishEvent,
21000
- onCellCursor: (cursor) => store.advanceCellEventCursor(conversation.id, cursor),
21001
19866
  finish: (status, reason) => {
21002
19867
  const updated = store.updateConversationStatus(conversation.id, runId, status, reason ? { reason } : {});
21003
19868
  if (updated) store.appendEvent(conversation.id, runId, status, reason ? { reason } : {});
21004
- if (updated) this.notifyTurnEnd(store, conversation.id, runId, status, reason, text, turnStartSequence);
21005
19869
  }
21006
19870
  };
21007
- const memory = this.getMemory();
21008
- if (memory) {
21009
- active.memory = { note: memory.brief(conversation.id, text), snapshot: memory.snapshot() };
21010
- const firstSequence = store.getConversation(conversation.id)?.lastEventSequence ?? 0;
21011
- active.beforeFinish = async () => {
21012
- const reply = collectAssistantText(store, conversation.id, runId, firstSequence);
21013
- const project = store.getConversationProject(conversation.id);
21014
- const changes = await memory.remember({ conversationId: conversation.id, project, userText: text, assistantText: reply });
21015
- if (changes.length) publishEvent("memory.updated", { items: changes });
21016
- };
21017
- }
21018
19871
  this.runtime.startTurn(active);
21019
19872
  json(res, 202, acceptedResponse);
21020
19873
  } catch {
@@ -21044,8 +19897,9 @@ var init_aster_api = __esm({
21044
19897
  publish("tool.activity", { tool: request.tool.name, activity: "rejected", reason: "unsafe_payload" });
21045
19898
  return "deny";
21046
19899
  }
19900
+ const workspaceRoot = conversation.cwd;
21047
19901
  try {
21048
- assertAsterCellPath(request.args.path);
19902
+ await assertAsterWorkspacePath(workspaceRoot, request.args.path, request.tool.name === "write");
21049
19903
  } catch {
21050
19904
  publish("tool.activity", { tool: request.tool.name, activity: "rejected", reason: "workspace_path_forbidden" });
21051
19905
  return "deny";
@@ -21070,16 +19924,6 @@ var init_aster_api = __esm({
21070
19924
  }
21071
19925
  store.updateConversationStatus(conversation.id, runId, "waiting_approval", { approvalId: approval.id });
21072
19926
  publish("approval.required", { approvalId: approval.id, tool: approval.toolName, explanation: approval.explanation, expiresAt: approval.expiresAt });
21073
- const push = this.getPush();
21074
- if (push?.configured) {
21075
- void push.notify({
21076
- title: "Approval required",
21077
- body: approval.toolName + ": " + approval.explanation,
21078
- conversationId: conversation.id,
21079
- category: "APPROVAL",
21080
- approvalId: approval.id
21081
- });
21082
- }
21083
19927
  return new Promise((resolveDecision) => {
21084
19928
  const timer3 = setTimeout(() => {
21085
19929
  void this.expireApproval(store, approval.id);
@@ -21136,115 +19980,6 @@ var init_aster_api = __esm({
21136
19980
  waiter.resolve(status === "expired" ? "deny" : decision);
21137
19981
  }
21138
19982
  }
21139
- async pauseConversation(res, store, conversation) {
21140
- const mapping = store.getCellMapping(conversation.id);
21141
- if (!mapping || !mapping.cellId) return sendError(res, 409, "cell_not_ready", "Conversation cell is not ready");
21142
- if (mapping.status === "paused") return json(res, 200, { ...publicConversation(conversation), cellStatus: "paused" });
21143
- if (conversation.activeRunId) return sendError(res, 409, "turn_active", "Pause is only available at a safe boundary; cancel or wait for the active turn first");
21144
- if (!this.runtime.pauseCell) return sendError(res, 503, "hotcell_pause_unavailable", "Hotcell pause is not configured");
21145
- try {
21146
- await this.runtime.pauseCell(mapping.cellId);
21147
- store.setCellStatus(conversation.id, "paused");
21148
- store.setConversationStatus(conversation.id, "paused", { reason: "safe_boundary" });
21149
- store.appendEvent(conversation.id, null, "conversation.paused", { strategy: "safe_boundary" });
21150
- const updated = store.getConversation(conversation.id);
21151
- json(res, 200, { ...publicConversation(updated), cellStatus: "paused" });
21152
- } catch {
21153
- sendError(res, 503, "pause_failed", "Conversation could not be paused; its lifecycle state was not changed");
21154
- }
21155
- }
21156
- async resumeConversation(res, store, conversation) {
21157
- const mapping = store.getCellMapping(conversation.id);
21158
- if (!mapping || !mapping.cellId) return sendError(res, 409, "cell_not_ready", "Conversation cell is not ready");
21159
- if (mapping.status !== "paused") return json(res, 200, { ...publicConversation(conversation), cellStatus: mapping.status });
21160
- if (!this.runtime.resumeCell) return sendError(res, 503, "hotcell_resume_unavailable", "Hotcell resume is not configured");
21161
- try {
21162
- await this.runtime.resumeCell(mapping.cellId);
21163
- store.setCellStatus(conversation.id, "ready");
21164
- store.setConversationStatus(conversation.id, "ready", { reason: "resumed" });
21165
- store.appendEvent(conversation.id, null, "conversation.resumed", { sessionId: mapping.sessionId });
21166
- const updated = store.getConversation(conversation.id);
21167
- json(res, 200, { ...publicConversation(updated), cellStatus: "ready" });
21168
- } catch {
21169
- sendError(res, 503, "resume_failed", "Conversation could not be resumed; the persisted cell state is unchanged");
21170
- }
21171
- }
21172
- async destroyConversation(res, store, conversation) {
21173
- const mapping = store.getCellMapping(conversation.id);
21174
- if (!mapping) return sendError(res, 404, "conversation_not_found", "Conversation not found");
21175
- if (mapping.status === "destroyed") return json(res, 200, { conversationId: conversation.id, status: "destroyed" });
21176
- if (!this.runtime.destroyCell) return sendError(res, 503, "hotcell_destroy_unavailable", "Hotcell cleanup is not configured");
21177
- store.setCellStatus(conversation.id, "destroying");
21178
- store.revokeGitHubGrant(conversation.id);
21179
- if (conversation.activeRunId) {
21180
- for (const approval of store.cancelPendingApprovals(conversation.id)) {
21181
- const waiter = this.waiters.get(approval.id);
21182
- if (waiter) {
21183
- clearTimeout(waiter.timer);
21184
- this.waiters.delete(approval.id);
21185
- waiter.resolve("deny");
21186
- }
21187
- }
21188
- this.runtime.cancelRun(conversation.activeRunId);
21189
- store.updateConversationStatus(conversation.id, conversation.activeRunId, "cancelled", { reason: "conversation_destroyed" });
21190
- }
21191
- try {
21192
- if (mapping.cellId) await this.runtime.destroyCell(mapping.cellId);
21193
- await unlink2(resolve13(sessionsDir(), mapping.sessionId + ".json")).catch(() => {
21194
- });
21195
- store.setCellStatus(conversation.id, "destroyed");
21196
- store.setConversationStatus(conversation.id, "destroyed");
21197
- store.appendEvent(conversation.id, null, "conversation.destroyed", { conversationId: conversation.id });
21198
- json(res, 200, { conversationId: conversation.id, status: "destroyed" });
21199
- } catch {
21200
- store.setCellStatus(conversation.id, "cleanup_pending");
21201
- sendError(res, 503, "cleanup_pending", "Cell cleanup is pending; retry DELETE to revoke credentials and remove the cell");
21202
- }
21203
- }
21204
- /** The conversation's cell id when it can take control requests; otherwise sends the error. */
21205
- readyCell(res, store, conversationId) {
21206
- const mapping = store.getCellMapping(conversationId);
21207
- if (mapping?.status === "paused") {
21208
- sendError(res, 409, "conversation_paused", "Conversation is paused; resume it first");
21209
- return void 0;
21210
- }
21211
- if (!mapping?.cellId || mapping.status !== "ready") {
21212
- sendError(res, 409, "conversation_cell_unavailable", "Conversation cell is not available");
21213
- return void 0;
21214
- }
21215
- return mapping.cellId;
21216
- }
21217
- /** Switch the cell session first, then the record, so a failed switch leaves both on the old model. */
21218
- async switchModel(res, store, conversation, model) {
21219
- if (conversation.activeRunId) {
21220
- sendError(res, 409, "conversation_busy", "A turn is already active for this conversation");
21221
- return void 0;
21222
- }
21223
- const cell = this.readyCell(res, store, conversation.id);
21224
- if (!cell) return void 0;
21225
- if (!this.runtime.setModel) {
21226
- sendError(res, 503, "model_switch_unavailable", "Model switching is not configured");
21227
- return void 0;
21228
- }
21229
- try {
21230
- await this.runtime.setModel(cell, model);
21231
- } catch {
21232
- sendError(res, 503, "model_switch_failed", "The conversation's model could not be switched");
21233
- return void 0;
21234
- }
21235
- if (!store.setConversationModel(conversation.id, model)) {
21236
- sendError(res, 409, "conversation_busy", "A turn is already active for this conversation");
21237
- return void 0;
21238
- }
21239
- try {
21240
- const sessionPath = resolve13(sessionsDir(), conversation.sessionId + ".json");
21241
- const sessionFile = await loadSession(sessionPath);
21242
- sessionFile.model = model;
21243
- await saveSession(sessionFile);
21244
- } catch {
21245
- }
21246
- return store.getConversation(conversation.id);
21247
- }
21248
19983
  async cancelConversation(res, store, conversation) {
21249
19984
  const runId = conversation.activeRunId;
21250
19985
  if (!runId) {
@@ -21287,16 +20022,6 @@ var init_aster_api = __esm({
21287
20022
  let last = cursor;
21288
20023
  let closed = false;
21289
20024
  let polling = false;
21290
- let released = false;
21291
- const connected = this.connectedEventStreams.get(conversationId) ?? 0;
21292
- this.connectedEventStreams.set(conversationId, connected + 1);
21293
- const release2 = () => {
21294
- if (released) return;
21295
- released = true;
21296
- const remaining = (this.connectedEventStreams.get(conversationId) ?? 1) - 1;
21297
- if (remaining > 0) this.connectedEventStreams.set(conversationId, remaining);
21298
- else this.connectedEventStreams.delete(conversationId);
21299
- };
21300
20025
  let lastHeartbeat = Date.now();
21301
20026
  const pump = async () => {
21302
20027
  if (closed || polling) return;
@@ -21318,7 +20043,6 @@ data: ${JSON.stringify(event.data)}
21318
20043
  }
21319
20044
  } catch {
21320
20045
  closed = true;
21321
- release2();
21322
20046
  res.end();
21323
20047
  } finally {
21324
20048
  polling = false;
@@ -21329,9 +20053,7 @@ data: ${JSON.stringify(event.data)}
21329
20053
  };
21330
20054
  res.on("close", () => {
21331
20055
  closed = true;
21332
- release2();
21333
20056
  });
21334
- res.on("finish", release2);
21335
20057
  void pump();
21336
20058
  }
21337
20059
  async recover(store) {
@@ -21368,8 +20090,7 @@ data: ${JSON.stringify(event.data)}
21368
20090
  async requireConversation(store, principal, id) {
21369
20091
  if (!CONVERSATION_ID_RE.test(id)) return void 0;
21370
20092
  const conversation = store.getConversation(id);
21371
- const mapping = store.getCellMapping(id);
21372
- return conversation && mapping?.credentialId === principal.credentialId && principal.workspaceIds.includes(conversation.workspaceId) ? conversation : void 0;
20093
+ return conversation && principal.workspaceIds.includes(conversation.workspaceId) ? conversation : void 0;
21373
20094
  }
21374
20095
  };
21375
20096
  }
@@ -21379,7 +20100,7 @@ data: ${JSON.stringify(event.data)}
21379
20100
  import { execFile as execFile8 } from "child_process";
21380
20101
  import { readFile as readFile26 } from "fs/promises";
21381
20102
  import { promisify as promisify7 } from "util";
21382
- import { randomUUID as randomUUID9 } from "crypto";
20103
+ import { randomUUID as randomUUID8 } from "crypto";
21383
20104
  async function writeCellFileChunked(cell, path, content) {
21384
20105
  if (Buffer.byteLength(content, "utf8") <= CELL_FILE_WRITE_CHUNK_CHARS) {
21385
20106
  await cell.writeFile(path, content, { mode: "0600" });
@@ -21575,7 +20296,7 @@ var init_aster_cell_runtime = __esm({
21575
20296
  await this.installRuntime(cell);
21576
20297
  await this.ensureBridge(cell);
21577
20298
  await this.control(cell, "POST", "/rpc", {
21578
- id: randomUUID9(),
20299
+ id: randomUUID8(),
21579
20300
  type: "new_session",
21580
20301
  sessionId: input.sessionId,
21581
20302
  config: { model: input.model, cwd: CELL_WORKSPACE }
@@ -21594,7 +20315,7 @@ var init_aster_cell_runtime = __esm({
21594
20315
  /** Switch the cell session's model between turns. The bridge persists it for session restore. */
21595
20316
  async setModel(cellId, model) {
21596
20317
  await this.ensureReady(cellId);
21597
- await this.controlById(cellId, "POST", "/rpc", { id: randomUUID9(), type: "set_model", modelId: model });
20318
+ await this.controlById(cellId, "POST", "/rpc", { id: randomUUID8(), type: "set_model", modelId: model });
21598
20319
  }
21599
20320
  /** Reinstall the runtime if the cell's image lost it and restart the bridge if it isn't running. */
21600
20321
  async ensureReady(cellId) {
@@ -21657,7 +20378,7 @@ var init_aster_cell_runtime = __esm({
21657
20378
  const active = this.activeRuns.get(runId);
21658
20379
  if (!active) return;
21659
20380
  active.stop();
21660
- void this.controlById(active.cellId, "POST", "/rpc", { id: randomUUID9(), type: "abort" }).catch(() => {
20381
+ void this.controlById(active.cellId, "POST", "/rpc", { id: randomUUID8(), type: "abort" }).catch(() => {
21661
20382
  });
21662
20383
  }
21663
20384
  async runTurn(turn, options = {}) {
@@ -21757,7 +20478,7 @@ ${turn.userText}` : turn.userText;
21757
20478
  args: event.args ?? {}
21758
20479
  });
21759
20480
  await this.controlById(cellId, "POST", "/rpc", {
21760
- id: randomUUID9(),
20481
+ id: randomUUID8(),
21761
20482
  type: "resolve_permission",
21762
20483
  requestId: event.requestId,
21763
20484
  decision
@@ -21984,7 +20705,7 @@ ${text}` : text, { mode: "0600" });
21984
20705
  }
21985
20706
  }
21986
20707
  async control(cell, method, path, body) {
21987
- const requestId = randomUUID9();
20708
+ const requestId = randomUUID8();
21988
20709
  await cell.mkdir(CELL_CONTROL_DIR, { parents: true });
21989
20710
  await writeCellFileChunked(cell, `${CELL_CONTROL_DIR}/${requestId}.json`, JSON.stringify({ method, path, body }));
21990
20711
  const result2 = await cell.exec(`${CONTROL_RUNNER} ${requestId}`);
@@ -22200,7 +20921,7 @@ var init_hotcell_provider = __esm({
22200
20921
  });
22201
20922
 
22202
20923
  // src/server/routes.ts
22203
- import { randomUUID as randomUUID10 } from "crypto";
20924
+ import { randomUUID as randomUUID9 } from "crypto";
22204
20925
  import { URL as URL2 } from "url";
22205
20926
  import { readFile as readFile27, unlink as unlink3 } from "fs/promises";
22206
20927
  import { resolve as resolve14, basename as basename7 } from "path";
@@ -22544,7 +21265,7 @@ function setupRoutes(config2, options = {}) {
22544
21265
  const executor = new ToolExecutor(tools);
22545
21266
  const { makeSessionId: makeSessionId2 } = await Promise.resolve().then(() => (init_sessions(), sessions_exports));
22546
21267
  const sessionId = makeSessionId2(task);
22547
- const runId = randomUUID10();
21268
+ const runId = randomUUID9();
22548
21269
  const worktreeManager = new RunWorktreeManager();
22549
21270
  let worktree;
22550
21271
  if (useWorktree) {
@@ -23153,10 +21874,10 @@ var server_exports = {};
23153
21874
  __export(server_exports, {
23154
21875
  startServer: () => startServer
23155
21876
  });
23156
- import { createServer as createServer2 } from "http";
21877
+ import { createServer } from "http";
23157
21878
  async function startServer(opts2) {
23158
21879
  const { handleRequest, cleanup } = setupRoutes(opts2.config, { asterRuntime: opts2.asterRuntime });
23159
- const server = createServer2((req, res) => {
21880
+ const server = createServer((req, res) => {
23160
21881
  const isAsterApi = req.url?.startsWith("/api/v1/") === true;
23161
21882
  if (!isAsterApi) {
23162
21883
  res.setHeader("Access-Control-Allow-Origin", "*");
@@ -24642,7 +23363,7 @@ __export(app_helpers_exports, {
24642
23363
  trackRecentFile: () => trackRecentFile
24643
23364
  });
24644
23365
  import { execSync as execSync3, spawn as spawn7 } from "child_process";
24645
- import { existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
23366
+ import { existsSync as existsSync8, readFileSync as readFileSync6, statSync as statSync5 } from "fs";
24646
23367
  import { join as join41 } from "path";
24647
23368
  import { platform as platform3 } from "os";
24648
23369
  function replaceQueuedPrompt(queue2, key, full, display) {
@@ -24756,7 +23477,7 @@ function buildFilePickerIgnoreList(cwd) {
24756
23477
  if (stats.size > MAX_GITIGNORE_SIZE) {
24757
23478
  return hardcoded;
24758
23479
  }
24759
- const content = readFileSync7(gitignorePath, "utf-8");
23480
+ const content = readFileSync6(gitignorePath, "utf-8");
24760
23481
  for (const line of content.split(/\r?\n/)) {
24761
23482
  const trimmed = line.trim();
24762
23483
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -34315,7 +33036,7 @@ __export(openrouter_oauth_exports, {
34315
33036
  startLoopbackListener: () => startLoopbackListener
34316
33037
  });
34317
33038
  import { createHash as createHash6, randomBytes as randomBytes3 } from "crypto";
34318
- import { createServer as createServer3 } from "http";
33039
+ import { createServer as createServer2 } from "http";
34319
33040
  import { hostname } from "os";
34320
33041
  function createPkce() {
34321
33042
  const verifier = randomBytes3(32).toString("base64url");
@@ -34371,7 +33092,7 @@ async function startLoopbackListener(opts2 = {}) {
34371
33092
  });
34372
33093
  code.catch(() => void 0);
34373
33094
  let settled = false;
34374
- const server = createServer3((req, res) => {
33095
+ const server = createServer2((req, res) => {
34375
33096
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
34376
33097
  if (url.pathname !== "/callback") {
34377
33098
  res.writeHead(404).end();
@@ -39775,7 +38496,7 @@ var init_run_compact = __esm({
39775
38496
 
39776
38497
  // src/commands/save.ts
39777
38498
  import { mkdir as mkdir18, writeFile as writeFile16, unlink as unlink5 } from "fs/promises";
39778
- import { dirname as dirname22 } from "path";
38499
+ import { dirname as dirname21 } from "path";
39779
38500
  async function saveCustomCommand(opts2) {
39780
38501
  const dir = opts2.source === "project" ? projectCommandsDir(opts2.cwd) : globalCommandsDir();
39781
38502
  const filepath = `${dir}/${opts2.name}.md`;
@@ -39786,7 +38507,7 @@ async function saveCustomCommand(opts2) {
39786
38507
  if (opts2.effort) data.effort = opts2.effort;
39787
38508
  const frontmatter = serializeFrontmatter(data);
39788
38509
  const content = frontmatter + opts2.template;
39789
- await mkdir18(dirname22(filepath), { recursive: true });
38510
+ await mkdir18(dirname21(filepath), { recursive: true });
39790
38511
  await writeFile16(filepath, content, "utf8");
39791
38512
  return { filepath };
39792
38513
  }
@@ -41999,21 +40720,6 @@ ${result2.output ?? "(output not captured)"}`,
41999
40720
  [processMessage, handleSlash, queue2, history, historyIndex, events]
42000
40721
  );
42001
40722
  submitRef.current = submit;
42002
- useEffect11(() => {
42003
- if (!bridge) return;
42004
- const seen = announcedSubagentsRef.current;
42005
- const fresh = runningSubagents.filter((w) => !seen.has(w.id));
42006
- if (fresh.length === 0) return;
42007
- for (const w of fresh) seen.add(w.id);
42008
- setEvents((evts) => [
42009
- ...evts,
42010
- ...fresh.map((w) => ({
42011
- kind: "info",
42012
- key: mkKey(),
42013
- text: `subagent #${w.index} started: ${w.task.replace(/\s+/g, " ").slice(0, 100)} \xB7 /agents to view or stop`
42014
- }))
42015
- ]);
42016
- }, [bridge, runningSubagents]);
42017
40723
  const textOf = (m) => !m ? "" : typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ") : "";
42018
40724
  const describeActivity = () => {
42019
40725
  const msgs = messagesRef.current;