@epoch-agent/server 0.10.1 → 0.12.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
@@ -1,7 +1,7 @@
1
1
  import { createServer } from 'http';
2
2
  import { createLogger, t, ApprovalRelay, QuestionRelay, serializeStream, withRoleScope, withModelScope, withToolScope, nextRunAt, observeBackgroundTasks, skillIndexDescription, SANDBOX_EXCLUDES, SANDBOX_COVERS, isSystemOpenable, backgroundTaskOutput, listBackgroundTasks, providerLabel, MAX_REASON_CHARS, MAX_OBJECTIVE_CHARS, MAX_GOAL_MAX_ROUNDS, DEFAULT_GOAL_MAX_ROUNDS, SCHEDULE_DEFAULT_ALLOWLIST, SCHEDULE_DEFAULTS, coalesceRecording, readRecording } from '@epoch-agent/runtime';
3
3
  import { timingSafeEqual, randomBytes } from 'crypto';
4
- import { PLAN_OUTCOMES, WIRE_AUTH_COOKIE, WIRE_AUTH_COOKIE_ATTRS, WIRE_AUTH_TOKEN_PARAM, extractAllMentions, MAX_ATTACH_FILES, utf8ByteLength, MAX_SESSION_REFS, attachSessionSurface, isWireRewindScope, WIRE_REWIND_SCOPES, mcpConnectorState, MAX_ATTACH_TOTAL_BYTES, MAX_ATTACH_BYTES, truncateToBudget, WIRE_LANG_PARAM, isLang, isQuestionAnswerMap, isProviderType, apiKeyEnvVar, WIRE_FILE_STAT_MAX, WIRE_SETTING_WRITE_LAYERS, isWirePlanAction, WIRE_PLAN_ACTIONS, isPermissionLevel, PERMISSION_LEVELS, conformsToWire, WIRE_TRUST_REQUEST_FIELDS, WIRE_FORGET_WORKSPACE_FIELDS, collectPendingApprovals, unfixedRules, isWireSettingWriteLayer, parseModelRef, WIRE_GOAL_ACTIONS, OPERATION_TYPES, isOperationType, WIRE_PLUGIN_UPLOAD_FORM_FIELDS } from '@epoch-agent/protocol';
4
+ import { PLAN_OUTCOMES, WIRE_AUTH_COOKIE, WIRE_AUTH_COOKIE_ATTRS, WIRE_AUTH_TOKEN_PARAM, extractAllMentions, MAX_ATTACH_FILES, utf8ByteLength, MAX_SESSION_REFS, attachSessionSurface, isWireRewindScope, WIRE_REWIND_SCOPES, mcpConnectorState, MAX_ATTACH_TOTAL_BYTES, MAX_ATTACH_BYTES, truncateToBudget, WIRE_LANG_PARAM, isLang, conformsToWire, WIRE_LAST_SESSION_FIELDS, isQuestionAnswerMap, isProviderType, apiKeyEnvVar, WIRE_FILE_STAT_MAX, WIRE_SETTING_WRITE_LAYERS, isWirePlanAction, WIRE_PLAN_ACTIONS, isPermissionLevel, PERMISSION_LEVELS, WIRE_TRUST_REQUEST_FIELDS, WIRE_FORGET_WORKSPACE_FIELDS, collectPendingApprovals, unfixedRules, isWireSettingWriteLayer, parseModelRef, WIRE_GOAL_ACTIONS, OPERATION_TYPES, isOperationType, WIRE_PLUGIN_UPLOAD_FORM_FIELDS } from '@epoch-agent/protocol';
5
5
  import { resolve, join, relative, isAbsolute, sep, dirname, basename, extname } from 'path';
6
6
  import { existsSync, realpathSync, statSync, mkdirSync, createReadStream, readdirSync, createWriteStream, accessSync, constants } from 'fs';
7
7
  import { stat, readFile, rm } from 'fs/promises';
@@ -197,6 +197,7 @@ var SessionRegistry = class {
197
197
  controller: null,
198
198
  turnStartedAt: null,
199
199
  lastFinish: null,
200
+ persisted: null,
200
201
  queue: [],
201
202
  lastActiveTick: ++this.tick
202
203
  });
@@ -376,6 +377,7 @@ function toWireSummary(id, hub, row, extra = {}) {
376
377
  turnStartedAt: state.turnStartedAt,
377
378
  lastFinish: state.lastFinish,
378
379
  role: extra.role ?? null,
380
+ archived: row?.archived ?? false,
379
381
  ...extra.snippet === void 0 ? {} : { snippet: extra.snippet }
380
382
  };
381
383
  }
@@ -388,7 +390,7 @@ function toWireSummaryWorkspace(bound, live, row) {
388
390
  }
389
391
  return row?.workspaceState === "none" ? { state: "none" } : { state: "unknown" };
390
392
  }
391
- function listSessionSummaries(hubStates, catalog, query, workspaceOf = () => null, roleOf = () => null, limit = DEFAULT_SESSION_LIST_LIMIT) {
393
+ function listSessionSummaries(hubStates, catalog, query, workspaceOf = () => null, roleOf = () => null, limit = DEFAULT_SESSION_LIST_LIMIT, archived = false) {
392
394
  const byId = new Map(hubStates.map((s) => [s.id, s]));
393
395
  if (query) {
394
396
  if (!catalog) return [];
@@ -407,7 +409,7 @@ function listSessionSummaries(hubStates, catalog, query, workspaceOf = () => nul
407
409
  }
408
410
  return hits;
409
411
  }
410
- const rows = catalog?.list(limit) ?? [];
412
+ const rows = catalog?.list(limit, archived ? { archived: true } : {}) ?? [];
411
413
  const summaries = rows.map(
412
414
  (row) => toWireSummary(row.id, byId.get(row.id), row, {
413
415
  workspace: workspaceOf(row.id),
@@ -415,7 +417,9 @@ function listSessionSummaries(hubStates, catalog, query, workspaceOf = () => nul
415
417
  })
416
418
  );
417
419
  const known = new Set(rows.map((r) => r.id));
418
- const fresh = hubStates.filter((s) => !known.has(s.id)).map((s) => toWireSummary(s.id, s, null, { workspace: workspaceOf(s.id), role: roleOf(s.id) }));
420
+ const fresh = archived ? [] : hubStates.filter((s) => !known.has(s.id)).filter((s) => catalog?.get(s.id)?.archived !== true).map(
421
+ (s) => toWireSummary(s.id, s, null, { workspace: workspaceOf(s.id), role: roleOf(s.id) })
422
+ );
419
423
  return [...fresh, ...summaries].sort(compareSummaries).slice(0, limit);
420
424
  }
421
425
  function compareSummaries(a, b) {
@@ -462,6 +466,9 @@ var SessionHub = class {
462
466
  isCooled(sessionId) {
463
467
  return this.registry.isCooled(sessionId);
464
468
  }
469
+ persistedThrough(sessionId) {
470
+ return this.registry.get(sessionId)?.persisted ?? null;
471
+ }
465
472
  stateOf(sessionId) {
466
473
  return this.registry.get(sessionId)?.state ?? "idle";
467
474
  }
@@ -539,21 +546,22 @@ var SessionHub = class {
539
546
  }
540
547
  const queued = { message, ...decorate ? { decorate } : {} };
541
548
  if (entry.state !== "idle") {
549
+ const steered = decorate === void 0 && entry.queue.every((q) => q.decorate === void 0);
542
550
  if (!this.registry.enqueue(entry, queued)) {
543
551
  return { ok: false, reason: "queue-full", state: entry.state };
544
552
  }
545
- return { ok: true, queued: true };
553
+ return { ok: true, queued: true, steered };
546
554
  }
547
555
  this.beginTurn(entry, queued);
548
556
  return { ok: true, queued: false };
549
557
  }
550
558
  abort(sessionId) {
551
559
  const entry = this.registry.get(sessionId);
552
- if (!entry) return false;
553
- entry.queue.length = 0;
554
- if (!entry.controller) return false;
560
+ if (!entry) return { running: false, returned: [] };
561
+ const returned = entry.queue.splice(0, entry.queue.length).map((q) => q.message);
562
+ if (!entry.controller) return { running: false, returned };
555
563
  entry.controller.abort();
556
- return true;
564
+ return { running: true, returned };
557
565
  }
558
566
  listPending(sessionId) {
559
567
  return this.registry.get(sessionId)?.relay.listPending() ?? [];
@@ -634,12 +642,19 @@ var SessionHub = class {
634
642
  const sessionId = entry.session.sessionId;
635
643
  const signal = entry.controller?.signal;
636
644
  try {
637
- const run = entry.session.run(queued.message, signal ? { signal } : {});
645
+ const run = entry.session.run(queued.message, {
646
+ ...signal ? { signal } : {},
647
+ steer: this.steerFrom(entry)
648
+ });
638
649
  const raw = queued.decorate ? queued.decorate(run) : run;
639
650
  for await (const event of serializeStream(raw, entry.relay, entry.questions)) {
640
651
  const blocking = event.type === "approval-request" || event.type === "question";
641
652
  if (blocking) this.requestOwner.set(event.requestId, sessionId);
642
653
  const frame = this.publish(sessionId, stripArtifactData(event));
654
+ const through = entry.session.persistedThrough?.() ?? null;
655
+ if (through !== null && through !== entry.persisted?.id) {
656
+ entry.persisted = { id: through, seq: frame.seq };
657
+ }
643
658
  if (event.type === "finish") {
644
659
  entry.lastFinish = toWireFinish(event, frame.ts, entry.turnStartedAt);
645
660
  }
@@ -654,10 +669,22 @@ var SessionHub = class {
654
669
  }
655
670
  this.registry.touch(sessionId);
656
671
  entry.turnStartedAt = null;
672
+ entry.persisted = null;
657
673
  this.setState(entry, "idle");
658
674
  this.drainQueue(entry);
659
675
  }
660
676
  }
677
+ steerFrom(entry) {
678
+ const runLength = () => {
679
+ let n = 0;
680
+ while (n < entry.queue.length && entry.queue[n].decorate === void 0) n += 1;
681
+ return n;
682
+ };
683
+ return {
684
+ pending: () => runLength() > 0,
685
+ take: () => entry.queue.splice(0, runLength()).map((q) => q.message)
686
+ };
687
+ }
661
688
  drainQueue(entry) {
662
689
  if (this.closed || !this.registry.has(entry.session.sessionId)) return;
663
690
  if (this.present === 0) {
@@ -1500,9 +1527,13 @@ function toWireSettingRow(row) {
1500
1527
  overridable: row.overridable,
1501
1528
  writes: row.writes.map(toWireSettingWrite),
1502
1529
  ...row.valueKind === void 0 ? {} : { valueKind: row.valueKind },
1503
- ...row.choices === void 0 ? {} : { choices: [...row.choices] }
1530
+ ...row.choices === void 0 ? {} : { choices: [...row.choices] },
1531
+ ...row.pending === void 0 ? {} : { pending: toWireSettingPending(row.pending) }
1504
1532
  };
1505
1533
  }
1534
+ function toWireSettingPending(pending) {
1535
+ return { ...toWireSettingWrite(pending), value: pending.value };
1536
+ }
1506
1537
  function toWireSettingStep(step) {
1507
1538
  return { layer: step.layer, ...step.value === void 0 ? {} : { value: step.value } };
1508
1539
  }
@@ -1604,11 +1635,18 @@ var APPROVAL_OUTCOMES = /* @__PURE__ */ new Set([
1604
1635
  function health(ctx, res) {
1605
1636
  sendJson(res, 200, { ok: true, version: ctx.version });
1606
1637
  }
1638
+ function lastSessionField(ctx) {
1639
+ const recorded = ctx.runtime.lastOpenSession?.get();
1640
+ if (!recorded) return {};
1641
+ if (!ctx.hub.has(recorded) && !ctx.runtime.sessions?.get(recorded)) return {};
1642
+ return { lastSessionId: recorded };
1643
+ }
1607
1644
  function config(ctx, res, lang) {
1608
1645
  const { runtime } = ctx;
1609
1646
  const bound = runtime.workspaces.of(runtime.sessionId);
1610
1647
  sendJson(res, 200, {
1611
1648
  sessionId: runtime.sessionId,
1649
+ ...lastSessionField(ctx),
1612
1650
  model: runtime.config.model,
1613
1651
  permission: runtime.config.permission,
1614
1652
  workspace: bound ? toWireSessionWorkspace(bound) : null,
@@ -1638,13 +1676,15 @@ function config(ctx, res, lang) {
1638
1676
  nativeDirPicker: ctx.nativeDirPicker
1639
1677
  });
1640
1678
  }
1641
- function listSessions(ctx, res, query, _lang) {
1679
+ function listSessions(ctx, res, query, _lang, archived = false) {
1642
1680
  const sessions = listSessionSummaries(
1643
1681
  ctx.hub.listSessions(),
1644
1682
  ctx.runtime.sessions,
1645
1683
  query ?? null,
1646
1684
  (id) => ctx.runtime.workspaces.of(id),
1647
- (id) => ctx.runtime.sessionFactory.get(id)?.role ?? null
1685
+ (id) => ctx.runtime.sessionFactory.get(id)?.role ?? null,
1686
+ void 0,
1687
+ archived
1648
1688
  );
1649
1689
  sendJson(res, 200, { sessions });
1650
1690
  }
@@ -1686,9 +1726,16 @@ async function deleteSession(ctx, res, sessionId, lang) {
1686
1726
  async function patchSession(ctx, req, res, sessionId, lang) {
1687
1727
  const body = await readJsonBody(req, void 0, lang);
1688
1728
  if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
1689
- const raw = asRecord(body.value).title;
1690
- const title = typeof raw === "string" ? raw.trim() : "";
1691
- if (title.length === 0) {
1729
+ const fields = asRecord(body.value);
1730
+ const rawTitle = fields["title"];
1731
+ const rawArchived = fields["archived"];
1732
+ const wantsTitle = rawTitle !== void 0;
1733
+ const wantsArchive = typeof rawArchived === "boolean";
1734
+ if (!wantsTitle && !wantsArchive) {
1735
+ return sendError(res, 400, "bad-patch", t("web.patch_needs_field", void 0, lang));
1736
+ }
1737
+ const title = typeof rawTitle === "string" ? rawTitle.trim() : "";
1738
+ if (wantsTitle && title.length === 0) {
1692
1739
  return sendError(res, 400, "bad-title", t("web.title_required", void 0, lang));
1693
1740
  }
1694
1741
  const catalog = ctx.runtime.sessions;
@@ -1700,8 +1747,11 @@ async function patchSession(ctx, req, res, sessionId, lang) {
1700
1747
  t("web.session_store_unavailable", void 0, lang)
1701
1748
  );
1702
1749
  }
1703
- const row = catalog.rename(sessionId, title);
1750
+ let row = wantsTitle ? catalog.rename(sessionId, title) : catalog.get(sessionId);
1751
+ const archivedBefore = row?.archived;
1752
+ if (row && wantsArchive) row = catalog.setArchived(sessionId, rawArchived);
1704
1753
  if (!row) return sendUnknownSession(res, sessionId, lang);
1754
+ if (wantsArchive && archivedBefore !== row.archived) ctx.hub.notifySessionsChanged(sessionId);
1705
1755
  const hub = ctx.hub.listSessions().find((s) => s.id === sessionId);
1706
1756
  sendJson(res, 200, {
1707
1757
  session: toWireSummary(sessionId, hub, row, {
@@ -1714,11 +1764,17 @@ function listMessages(ctx, res, sessionId, lang) {
1714
1764
  if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
1715
1765
  return sendUnknownSession(res, sessionId, lang);
1716
1766
  }
1717
- const messages = ctx.runtime.sessionStore?.loadMessages(sessionId) ?? [];
1767
+ const all = ctx.runtime.sessionStore?.loadMessages(sessionId) ?? [];
1718
1768
  const usage = ctx.runtime.sessionUsage(sessionId);
1769
+ const flight = ctx.hub.persistedThrough(sessionId);
1770
+ const openFrom = ctx.runtime.sessionStore?.openTurnFrom(sessionId) ?? null;
1771
+ const messages = flight ? all.filter((m) => m.id <= flight.id) : all;
1772
+ const interrupted = flight === null && openFrom !== null ? openFrom : void 0;
1719
1773
  sendJson(res, 200, {
1720
1774
  messages,
1721
- ...usage ? { usage } : {}
1775
+ ...usage ? { usage } : {},
1776
+ ...flight ? { inFlightThroughSeq: flight.seq } : {},
1777
+ ...interrupted !== void 0 ? { interruptedTurnFrom: interrupted } : {}
1722
1778
  });
1723
1779
  }
1724
1780
  async function postMessage(ctx, req, res, sessionId, lang) {
@@ -1752,6 +1808,7 @@ async function postMessage(ctx, req, res, sessionId, lang) {
1752
1808
  return sendJson(res, 202, {
1753
1809
  accepted: true,
1754
1810
  queued: started.queued,
1811
+ ...started.steered === void 0 ? {} : { steered: started.steered },
1755
1812
  ...expansion.notice ? { command: expansion.notice } : {},
1756
1813
  ...attached.references.length > 0 ? { references: attached.references } : {},
1757
1814
  ...attached.files.length > 0 ? { files: attached.files } : {}
@@ -1773,7 +1830,11 @@ function pickTurnRole(live, raw) {
1773
1830
  }
1774
1831
  function abortSession(ctx, res, sessionId, lang) {
1775
1832
  if (!ctx.hub.has(sessionId)) return sendUnknownSession(res, sessionId, lang);
1776
- sendJson(res, 200, { aborted: ctx.hub.abort(sessionId) });
1833
+ const stopped = ctx.hub.abort(sessionId);
1834
+ sendJson(res, 200, {
1835
+ aborted: stopped.running,
1836
+ ...stopped.returned.length > 0 ? { returned: stopped.returned } : {}
1837
+ });
1777
1838
  }
1778
1839
  function listApprovals(ctx, res, sessionId, lang) {
1779
1840
  if (!ctx.hub.has(sessionId)) return sendUnknownSession(res, sessionId, lang);
@@ -2088,6 +2149,9 @@ async function rewind(ctx, req, res, sessionId, lang) {
2088
2149
  const input = readRewindInput(body.value, lang);
2089
2150
  if (!input.ok) return sendError(res, 400, input.code, input.message);
2090
2151
  const { turnIndex, scope, overwrite } = input.value;
2152
+ if (scope !== "files" && ctx.hub.stateOf(sessionId) !== "idle") {
2153
+ return sendError(res, 409, "turn-running", t("web.rewind_turn_running", void 0, lang));
2154
+ }
2091
2155
  const result = await control.rewind(turnIndex, {
2092
2156
  scope,
2093
2157
  ...overwrite === void 0 ? {} : { overwrite }
@@ -2284,6 +2348,32 @@ function listBlockedGoals(ctx, res, _lang) {
2284
2348
  );
2285
2349
  sendJson(res, 200, { items });
2286
2350
  }
2351
+ async function putLastSession(ctx, req, res, lang) {
2352
+ const control = ctx.runtime.lastOpenSession;
2353
+ if (!control) {
2354
+ return sendError(
2355
+ res,
2356
+ 503,
2357
+ "no-last-session-store",
2358
+ t("web.last_session_unavailable", {}, lang)
2359
+ );
2360
+ }
2361
+ const body = await readJsonBody(req, void 0, lang);
2362
+ if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
2363
+ if (!conformsToWire(body.value, WIRE_LAST_SESSION_FIELDS)) {
2364
+ return sendError(res, 400, "bad-request", t("web.last_session_bad_request", {}, lang));
2365
+ }
2366
+ const { sessionId } = body.value;
2367
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
2368
+ return sendError(res, 400, "bad-request", t("web.last_session_bad_request", {}, lang));
2369
+ }
2370
+ if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
2371
+ return sendUnknownSession(res, sessionId, lang);
2372
+ }
2373
+ const failure = control.set(sessionId);
2374
+ if (failure) return sendError(res, 500, "write-failed", failure(lang));
2375
+ sendJson(res, 200, { ok: true });
2376
+ }
2287
2377
  function readConfig(ctx, res, lang) {
2288
2378
  if (ctx.lanExposed) return sendLanExposed(res, lang);
2289
2379
  const outcome = ctx.runtime.mcp.readConfig();
@@ -2959,6 +3049,8 @@ function sendUploadPreview(res, outcome) {
2959
3049
  ok: false,
2960
3050
  plan: null,
2961
3051
  token: null,
3052
+ prefill: null,
3053
+ fromManifest: null,
2962
3054
  reason: outcome.reason,
2963
3055
  detail: outcome.detail
2964
3056
  });
@@ -2977,6 +3069,8 @@ function sendUploadPreview(res, outcome) {
2977
3069
  skippedLinks: [...skippedLinks]
2978
3070
  },
2979
3071
  token: outcome.token,
3072
+ prefill: outcome.prefill,
3073
+ fromManifest: [...outcome.fromManifest],
2980
3074
  reason: null,
2981
3075
  detail: null
2982
3076
  });
@@ -3142,6 +3236,39 @@ function addFailure2(reason, detail, lang) {
3142
3236
  function stringList2(value) {
3143
3237
  return value.filter((v) => typeof v === "string");
3144
3238
  }
3239
+ async function removeRole(ctx, req, res, lang) {
3240
+ if (ctx.lanExposed) {
3241
+ return sendError(
3242
+ res,
3243
+ 403,
3244
+ "role-remove-lan-exposed",
3245
+ t("web.role_remove_lan_exposed", void 0, lang)
3246
+ );
3247
+ }
3248
+ const body = await readJsonBody(req);
3249
+ if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
3250
+ const payload = asRecord(body.value);
3251
+ const name = typeof payload["name"] === "string" ? payload["name"].trim() : "";
3252
+ const outcome = ctx.runtime.roleWrite.remove(name, lang);
3253
+ if (!outcome.ok) {
3254
+ return sendJson(res, 200, {
3255
+ ok: false,
3256
+ name: "",
3257
+ path: null,
3258
+ takenOverBy: null,
3259
+ detail: outcome.detail,
3260
+ reason: outcome.reason
3261
+ });
3262
+ }
3263
+ sendJson(res, 200, {
3264
+ ok: true,
3265
+ name: outcome.name,
3266
+ path: outcome.path,
3267
+ takenOverBy: outcome.takenOverBy,
3268
+ detail: null,
3269
+ reason: null
3270
+ });
3271
+ }
3145
3272
  function stringList3(value) {
3146
3273
  if (!Array.isArray(value)) return null;
3147
3274
  const out = [];
@@ -3361,6 +3488,12 @@ function listPending(ctx, res, lang) {
3361
3488
  const items = collectPendingApprovals(control.list(), control.recentRuns(RECENT_RUNS_LIMIT));
3362
3489
  sendJson(res, 200, { items });
3363
3490
  }
3491
+ function listRunning(ctx, res, lang) {
3492
+ const control = schedulesOf(ctx, res, lang);
3493
+ if (!control) return;
3494
+ const items = control.running().map(({ scheduleId, startedAt }) => ({ scheduleId, startedAt }));
3495
+ sendJson(res, 200, { items });
3496
+ }
3364
3497
  function getSchedule(ctx, res, id, lang) {
3365
3498
  const control = schedulesOf(ctx, res, lang);
3366
3499
  if (!control) return;
@@ -3394,10 +3527,20 @@ async function patchSchedule(ctx, req, res, id, lang) {
3394
3527
  if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
3395
3528
  const parsed = parseUpdate2(body.value, lang);
3396
3529
  if (!parsed.ok) return sendError(res, 400, "bad-schedule", parsed.message);
3397
- const saved = await control.update(id, parsed.value, {
3398
- bypassAcknowledged: bypassAcknowledged(body.value)
3399
- });
3400
- sendJson(res, 200, toSaveResponse(control, saved));
3530
+ const { enabled, ...rest } = parsed.value;
3531
+ const saved = Object.keys(rest).length > 0 ? await control.update(id, rest, { bypassAcknowledged: bypassAcknowledged(body.value) }) : { issues: [] };
3532
+ if (saved.issues.length > 0) return sendJson(res, 200, toSaveResponse(control, saved));
3533
+ const registration = enabled === void 0 ? saved.registration : await control.setEnabled(id, enabled);
3534
+ const next = control.get(id) ?? saved.schedule;
3535
+ sendJson(
3536
+ res,
3537
+ 200,
3538
+ toSaveResponse(control, {
3539
+ ...next ? { schedule: next } : {},
3540
+ issues: [],
3541
+ ...registration ? { registration } : {}
3542
+ })
3543
+ );
3401
3544
  }
3402
3545
  async function deleteSchedule(ctx, res, id, lang) {
3403
3546
  const control = schedulesOf(ctx, res, lang);
@@ -4738,6 +4881,9 @@ async function handleApi(opts, req, res, method, segments, lang) {
4738
4881
  return config(ctx, res, lang);
4739
4882
  }
4740
4883
  if (resource === "events" && method === "GET") return openStream(opts, req, res);
4884
+ if (resource === "last-session" && first === void 0 && method === "PUT") {
4885
+ return putLastSession(ctx, req, res, lang);
4886
+ }
4741
4887
  if (resource === "sessions") {
4742
4888
  return handleSessions(opts, req, res, method, first, second, detail, lang);
4743
4889
  }
@@ -4768,6 +4914,9 @@ async function handleApi(opts, req, res, method, segments, lang) {
4768
4914
  if (resource === "roles" && first === void 0 && method === "POST") {
4769
4915
  return addRole(ctx, req, res, lang);
4770
4916
  }
4917
+ if (resource === "roles" && first === "remove" && method === "POST") {
4918
+ return removeRole(ctx, req, res, lang);
4919
+ }
4771
4920
  if (resource === "skills" && first === "import" && second === "preview" && method === "POST") {
4772
4921
  return previewSkillImport(ctx, req, res, lang);
4773
4922
  }
@@ -4865,6 +5014,10 @@ async function handleSchedules(ctx, req, res, method, scheduleId, action, runId,
4865
5014
  if (isRead(method)) return listPending(ctx, res, lang);
4866
5015
  return methodNotAllowed(res, "GET", lang);
4867
5016
  }
5017
+ if (action === void 0 && scheduleId === "running") {
5018
+ if (isRead(method)) return listRunning(ctx, res, lang);
5019
+ return methodNotAllowed(res, "GET", lang);
5020
+ }
4868
5021
  if (action === void 0) {
4869
5022
  if (isRead(method)) return getSchedule(ctx, res, scheduleId, lang);
4870
5023
  if (method === "PATCH") return patchSchedule(ctx, req, res, scheduleId, lang);
@@ -4888,7 +5041,9 @@ async function handleSchedules(ctx, req, res, method, scheduleId, action, runId,
4888
5041
  async function handleSessions(opts, req, res, method, sessionId, action, detail, lang) {
4889
5042
  const { ctx } = opts;
4890
5043
  if (sessionId === void 0) {
4891
- if (isRead(method)) return listSessions(ctx, res, searchQuery(req.url));
5044
+ if (isRead(method)) {
5045
+ return listSessions(ctx, res, searchQuery(req.url), lang, archivedFlag(req.url));
5046
+ }
4892
5047
  if (method === "POST") return createSession(ctx, req, res, lang);
4893
5048
  return methodNotAllowed(res, "GET, POST", lang);
4894
5049
  }
@@ -5025,6 +5180,10 @@ function searchQuery(rawUrl) {
5025
5180
  const value = new URL(rawUrl ?? "/", "http://127.0.0.1").searchParams.get("q")?.trim();
5026
5181
  return value ? value : void 0;
5027
5182
  }
5183
+ function archivedFlag(rawUrl) {
5184
+ const raw = new URL(rawUrl ?? "/", "http://127.0.0.1").searchParams.get("archived");
5185
+ return raw === "1" || raw === "true";
5186
+ }
5028
5187
  function redirectWithoutToken(res, rawUrl) {
5029
5188
  const url = new URL(rawUrl ?? "/", "http://127.0.0.1");
5030
5189
  url.searchParams.delete(WIRE_AUTH_TOKEN_PARAM);