@epoch-agent/server 0.10.1 → 0.11.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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Lang, WireSkillImportFailure, WireSkillImportForm, WireSkillRemoveFailure, WireSkillStageFailure, WireProjectInventory, WireCapabilitiesResponse, WireSkillBodyResponse, EpochUserContent, AgentEvent, WireEnvelope, CustomCommandDef, ExpandedCommand, ModelRef, SetSelectionResult, WireCommandExpansion, WireCustomCommand, WireGoalPhase, WireGoalRefusal, WireTurnState, WireSessionFinish, ApprovalOutcome, QuestionAnswer, SessionSurfaceView, WireSessionReference, WireFileReference, ProviderType, ModelSelectionOrigin, WireModelCaveat, WireModelRejection, ContextBreakdown, PermissionLevel, WirePermissionRememberFailure, WirePluginSourceType, WirePluginVisibility, WirePluginStatus, WirePluginRefuseReason, WirePluginUploadFieldName, WirePluginUploadFields, WirePluginEntry, WireModelSource, WireModelProbeStatus, ScheduleDefinition, ScheduleRun, ScheduleBackendKind, OperationType, ScheduleTrigger, WireScheduleIssue, ScheduleRunStatus, TrustLevel, WireSandboxReason, WireAuditOutcome, WireAuditCode, WireCachedApproval, WireCachedDecision, WireToolVerdict, WireToolGateAxis, WireSecurityResponse, WireRewindScope, WireRewindRequest, WireCheckpointSummary, WireRewindPreview, WireRewindResponse, ActiveAgentRole, WireSettingValue, WireSettingLayer, WireSettingWriteLayer, WireSettingFutile, WireSettingApply, WireSettingValueKind, WireSettingsResponse, EpochConfig, TokenUsage, WireCompressionLine, WireProviderSummary, WireToolSummary, Diagnostic, WireReplayMessage, WireFileChange, WireAuthGuard, WireWorkspaceDiffResponse, BackgroundTaskInfo, WireTasksResponse, WireBackgroundTask, WireScheduleRow } from '@epoch-agent/protocol';
2
- import { SerializableApprovalRequest, SerializableQuestionRequest, ApprovalRevokeResult, LevelChangeResult, WorkspaceFilesView } from '@epoch-agent/runtime';
1
+ import { Lang, WireSkillImportFailure, WireSkillImportForm, WireSkillRemoveFailure, WireSkillStageFailure, WireProjectInventory, WireCapabilitiesResponse, WireSkillBodyResponse, EpochUserContent, AgentEvent, WireEnvelope, CustomCommandDef, ExpandedCommand, ModelRef, SetSelectionResult, WireCommandExpansion, WireCustomCommand, WireGoalPhase, WireGoalRefusal, WireTurnState, WireSessionFinish, ApprovalOutcome, QuestionAnswer, SessionSurfaceView, WireSessionReference, WireFileReference, ProviderType, ModelSelectionOrigin, WireModelCaveat, WireModelRejection, ContextBreakdown, PermissionLevel, WirePermissionRememberFailure, WirePluginSourceType, WirePluginVisibility, WirePluginStatus, WirePluginRefuseReason, WirePluginUploadForm, WirePluginUploadFieldName, WirePluginUploadFields, WirePluginEntry, WireModelSource, WireModelProbeStatus, ScheduleDefinition, ScheduleRun, ScheduleBackendKind, OperationType, ScheduleTrigger, WireScheduleIssue, ScheduleRunStatus, TrustLevel, WireSandboxReason, WireAuditOutcome, WireAuditCode, WireCachedApproval, WireCachedDecision, WireToolVerdict, WireToolGateAxis, WireSecurityResponse, WireRewindScope, WireRewindRequest, WireCheckpointSummary, WireRewindPreview, WireRewindResponse, ActiveAgentRole, WireSettingValue, WireSettingLayer, WireSettingWriteLayer, WireSettingFutile, WireSettingApply, WireSettingValueKind, WireSettingsResponse, EpochConfig, TokenUsage, WireCompressionLine, WireProviderSummary, WireToolSummary, Diagnostic, WireReplayMessage, WireFileChange, WireAuthGuard, WireWorkspaceDiffResponse, BackgroundTaskInfo, WireTasksResponse, WireBackgroundTask, WireScheduleRow } from '@epoch-agent/protocol';
2
+ import { SerializableApprovalRequest, SerializableQuestionRequest, RunningSchedule, ApprovalRevokeResult, LevelChangeResult, LastOpenSessionControl, WorkspaceFilesView } from '@epoch-agent/runtime';
3
3
  import { ServerResponse } from 'node:http';
4
4
 
5
5
  /**
@@ -2869,6 +2869,21 @@ type UploadPreviewOutcomeView = {
2869
2869
  ok: true;
2870
2870
  plan: PackPlanView;
2871
2871
  token: string;
2872
+ /**
2873
+ * 那张表单每一格**从什么值开始**(2026-09-15)。
2874
+ *
2875
+ * ⚠️ **这一层原样转,一格都不算**(口径同这个文件头那句「一条策略判断
2876
+ * 都没有」):四档优先级(宿主锁死 > 宿主初值 > 包里同名格 > 空)在
2877
+ * runtime 的 `resolveUploadPrefill` 上,在这儿兜一次的表现是同一格
2878
+ * 在两处得出两个值。
2879
+ *
2880
+ * ⚠️ 键取自 `WirePluginUploadFieldName` 而不是自造一套:这一份最终原样
2881
+ * 落到 `WirePluginUploadPreviewResponse.prefill` 上,两套名字会逼出一张
2882
+ * 会错的映射表(判据同 runtime 的 `PLUGIN_UPLOAD_FIELDS`)。
2883
+ */
2884
+ prefill: WirePluginUploadForm;
2885
+ /** {@link prefill} 里值是从包里读出来的那几格。只管屏幕上那一句提示 */
2886
+ fromManifest: readonly WirePluginUploadFieldName[];
2872
2887
  } | RefusalView;
2873
2888
  /**
2874
2889
  * 上传的结局。
@@ -3259,6 +3274,13 @@ interface ScheduleControlView {
3259
3274
  runs: (id: string, limit?: number) => ScheduleRun[];
3260
3275
  /** 全部任务的最近运行 —— 「运行记录」那个 tab 的素材 */
3261
3276
  recentRuns: (limit?: number) => ScheduleRun[];
3277
+ /**
3278
+ * **此刻**哪几条在跑(2026-09-15)。素材是那张锁表,判定(心跳 + pid)留在 core。
3279
+ *
3280
+ * ⚠️ **它和上面两格答的不是同一个问题**:那两格要等一轮**跑完**才有行
3281
+ * (执行器收尾时才 `recordRun`),所以「正在跑」在它们眼里是不存在的。
3282
+ */
3283
+ running: () => readonly RunningSchedule[];
3262
3284
  capability: () => Promise<ScheduleCapabilityView>;
3263
3285
  create: (input: ScheduleCreateView, opts?: {
3264
3286
  register?: boolean;
@@ -4252,6 +4274,13 @@ interface CatalogRow {
4252
4274
  workspaceState?: 'bound' | 'none';
4253
4275
  /** `workspaceState === 'bound'` 那一档的目录 */
4254
4276
  workspaceRoot?: string;
4277
+ /**
4278
+ * 被收进归档了吗(方案 64 PR-2)。
4279
+ *
4280
+ * **必填**:这一列从会话库 v1 起就有、`NOT NULL DEFAULT 0`,
4281
+ * 每一行都答得出。上面那两格可选说的是另一件事(v7 之前的老行没记过)。
4282
+ */
4283
+ archived: boolean;
4255
4284
  }
4256
4285
  /** 一条 FTS5 命中 —— runtime 的 `SessionHit` 结构上满足它 */
4257
4286
  interface CatalogHit {
@@ -4273,9 +4302,17 @@ interface CatalogDeletion {
4273
4302
  * 起不来(SQLite 挂了)时整个是 `null`,列表退化成「只有 Hub 里那几个」。
4274
4303
  */
4275
4304
  interface SessionCatalog {
4276
- list(limit?: number): CatalogRow[];
4305
+ /**
4306
+ * ⚠️ `archived` 是三态(方案 64 PR-2):不给 = 只列没归档的,
4307
+ * `true` = **只**列归档的。判据同 runtime 的 `SessionControl.list`。
4308
+ */
4309
+ list(limit?: number, opts?: {
4310
+ archived?: boolean;
4311
+ }): CatalogRow[];
4277
4312
  get(sessionId: string): CatalogRow | null;
4278
4313
  rename(sessionId: string, title: string): CatalogRow | null;
4314
+ /** 收进归档 / 取回。**不是删除** —— 判据在 runtime 的 `SessionControl.setArchived` 上 */
4315
+ setArchived(sessionId: string, archived: boolean): CatalogRow | null;
4279
4316
  delete(sessionId: string): Promise<CatalogDeletion>;
4280
4317
  search(query: string, limit?: number): CatalogHit[];
4281
4318
  }
@@ -4704,6 +4741,22 @@ interface WebRuntimeView extends RuntimeCapabilities, RuntimeSecurity, RuntimeSe
4704
4741
  * 而 DELETE / PATCH 回 503 —— 不是假装成功。
4705
4742
  */
4706
4743
  sessions: SessionCatalog | null;
4744
+ /**
4745
+ * 「这个项目上次停在哪段会话」(2026-09-14)。`EpochRuntime.lastOpenSession`
4746
+ * 结构上正好满足它。
4747
+ *
4748
+ * ## ⚠️ 可选,而这一档和 `sessions` 那个 `| null` 不是同一种「没有」
4749
+ *
4750
+ * 那一格是「SQLite 起不来」(这个进程有这项能力,只是坏了)。这一格是
4751
+ * **自己手拼 `WebRuntimeView` 的宿主压根没给** —— 嵌入宿主和
4752
+ * `__tests__/harness.ts` 里那份假 runtime 都是手拼的,写成必填等于这一笔
4753
+ * 改动把它们全打断,而它们不给这一格照样能跑(首屏退回引导会话,逐字等于
4754
+ * 这一轮之前的行为)。
4755
+ *
4756
+ * 没给时:`GET /api/config` 不带 `lastSessionId`,`PUT /api/last-session`
4757
+ * 回 503 —— **不是假装记下来了**(同 `sessions` 为 null 时那两条的口径)。
4758
+ */
4759
+ lastOpenSession?: LastOpenSessionControl;
4707
4760
  /**
4708
4761
  * `@:` 引用另一段会话那一片(方案 53 PR-4)。SQLite 起不来时为 null。
4709
4762
  * `EpochRuntime.sessionReferences` 结构上正好满足它。
@@ -5467,12 +5520,13 @@ declare function toWireTask(sessionId: string, info: BackgroundTaskInfo, registr
5467
5520
  declare function collectTasks(sessionId: string, registry: TaskRegistryView): WireTasksResponse;
5468
5521
 
5469
5522
  /**
5470
- * 定时任务那十条端点([方案 45](../../../../docs/verify/VERIFY_RECORD-45-automation.md)
5471
- * PR-3 九条 + PR-4 那条 `pending`)。
5523
+ * 定时任务那十一条端点([方案 45](../../../../docs/verify/VERIFY_RECORD-45-automation.md)
5524
+ * PR-3 九条 + PR-4 那条 `pending` + 2026-09-15 那条 `running`)。
5472
5525
  *
5473
5526
  * ```
5474
5527
  * GET /api/schedules 两个 tab 一次取齐
5475
5528
  * GET /api/schedules/pending 还欠着的那几张欠条(PR-4)
5529
+ * GET /api/schedules/running 此刻哪几条在跑(2026-09-15)
5476
5530
  * POST /api/schedules 建一条
5477
5531
  * GET /api/schedules/:id 一条的全部字段
5478
5532
  * PATCH /api/schedules/:id 改一条(含那个开关)
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';
@@ -376,6 +376,7 @@ function toWireSummary(id, hub, row, extra = {}) {
376
376
  turnStartedAt: state.turnStartedAt,
377
377
  lastFinish: state.lastFinish,
378
378
  role: extra.role ?? null,
379
+ archived: row?.archived ?? false,
379
380
  ...extra.snippet === void 0 ? {} : { snippet: extra.snippet }
380
381
  };
381
382
  }
@@ -388,7 +389,7 @@ function toWireSummaryWorkspace(bound, live, row) {
388
389
  }
389
390
  return row?.workspaceState === "none" ? { state: "none" } : { state: "unknown" };
390
391
  }
391
- function listSessionSummaries(hubStates, catalog, query, workspaceOf = () => null, roleOf = () => null, limit = DEFAULT_SESSION_LIST_LIMIT) {
392
+ function listSessionSummaries(hubStates, catalog, query, workspaceOf = () => null, roleOf = () => null, limit = DEFAULT_SESSION_LIST_LIMIT, archived = false) {
392
393
  const byId = new Map(hubStates.map((s) => [s.id, s]));
393
394
  if (query) {
394
395
  if (!catalog) return [];
@@ -407,7 +408,7 @@ function listSessionSummaries(hubStates, catalog, query, workspaceOf = () => nul
407
408
  }
408
409
  return hits;
409
410
  }
410
- const rows = catalog?.list(limit) ?? [];
411
+ const rows = catalog?.list(limit, archived ? { archived: true } : {}) ?? [];
411
412
  const summaries = rows.map(
412
413
  (row) => toWireSummary(row.id, byId.get(row.id), row, {
413
414
  workspace: workspaceOf(row.id),
@@ -415,7 +416,9 @@ function listSessionSummaries(hubStates, catalog, query, workspaceOf = () => nul
415
416
  })
416
417
  );
417
418
  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) }));
419
+ const fresh = archived ? [] : hubStates.filter((s) => !known.has(s.id)).filter((s) => catalog?.get(s.id)?.archived !== true).map(
420
+ (s) => toWireSummary(s.id, s, null, { workspace: workspaceOf(s.id), role: roleOf(s.id) })
421
+ );
419
422
  return [...fresh, ...summaries].sort(compareSummaries).slice(0, limit);
420
423
  }
421
424
  function compareSummaries(a, b) {
@@ -1604,11 +1607,18 @@ var APPROVAL_OUTCOMES = /* @__PURE__ */ new Set([
1604
1607
  function health(ctx, res) {
1605
1608
  sendJson(res, 200, { ok: true, version: ctx.version });
1606
1609
  }
1610
+ function lastSessionField(ctx) {
1611
+ const recorded = ctx.runtime.lastOpenSession?.get();
1612
+ if (!recorded) return {};
1613
+ if (!ctx.hub.has(recorded) && !ctx.runtime.sessions?.get(recorded)) return {};
1614
+ return { lastSessionId: recorded };
1615
+ }
1607
1616
  function config(ctx, res, lang) {
1608
1617
  const { runtime } = ctx;
1609
1618
  const bound = runtime.workspaces.of(runtime.sessionId);
1610
1619
  sendJson(res, 200, {
1611
1620
  sessionId: runtime.sessionId,
1621
+ ...lastSessionField(ctx),
1612
1622
  model: runtime.config.model,
1613
1623
  permission: runtime.config.permission,
1614
1624
  workspace: bound ? toWireSessionWorkspace(bound) : null,
@@ -1638,13 +1648,15 @@ function config(ctx, res, lang) {
1638
1648
  nativeDirPicker: ctx.nativeDirPicker
1639
1649
  });
1640
1650
  }
1641
- function listSessions(ctx, res, query, _lang) {
1651
+ function listSessions(ctx, res, query, _lang, archived = false) {
1642
1652
  const sessions = listSessionSummaries(
1643
1653
  ctx.hub.listSessions(),
1644
1654
  ctx.runtime.sessions,
1645
1655
  query ?? null,
1646
1656
  (id) => ctx.runtime.workspaces.of(id),
1647
- (id) => ctx.runtime.sessionFactory.get(id)?.role ?? null
1657
+ (id) => ctx.runtime.sessionFactory.get(id)?.role ?? null,
1658
+ void 0,
1659
+ archived
1648
1660
  );
1649
1661
  sendJson(res, 200, { sessions });
1650
1662
  }
@@ -1686,9 +1698,16 @@ async function deleteSession(ctx, res, sessionId, lang) {
1686
1698
  async function patchSession(ctx, req, res, sessionId, lang) {
1687
1699
  const body = await readJsonBody(req, void 0, lang);
1688
1700
  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) {
1701
+ const fields = asRecord(body.value);
1702
+ const rawTitle = fields["title"];
1703
+ const rawArchived = fields["archived"];
1704
+ const wantsTitle = rawTitle !== void 0;
1705
+ const wantsArchive = typeof rawArchived === "boolean";
1706
+ if (!wantsTitle && !wantsArchive) {
1707
+ return sendError(res, 400, "bad-patch", t("web.patch_needs_field", void 0, lang));
1708
+ }
1709
+ const title = typeof rawTitle === "string" ? rawTitle.trim() : "";
1710
+ if (wantsTitle && title.length === 0) {
1692
1711
  return sendError(res, 400, "bad-title", t("web.title_required", void 0, lang));
1693
1712
  }
1694
1713
  const catalog = ctx.runtime.sessions;
@@ -1700,7 +1719,8 @@ async function patchSession(ctx, req, res, sessionId, lang) {
1700
1719
  t("web.session_store_unavailable", void 0, lang)
1701
1720
  );
1702
1721
  }
1703
- const row = catalog.rename(sessionId, title);
1722
+ let row = wantsTitle ? catalog.rename(sessionId, title) : catalog.get(sessionId);
1723
+ if (row && wantsArchive) row = catalog.setArchived(sessionId, rawArchived);
1704
1724
  if (!row) return sendUnknownSession(res, sessionId, lang);
1705
1725
  const hub = ctx.hub.listSessions().find((s) => s.id === sessionId);
1706
1726
  sendJson(res, 200, {
@@ -2284,6 +2304,32 @@ function listBlockedGoals(ctx, res, _lang) {
2284
2304
  );
2285
2305
  sendJson(res, 200, { items });
2286
2306
  }
2307
+ async function putLastSession(ctx, req, res, lang) {
2308
+ const control = ctx.runtime.lastOpenSession;
2309
+ if (!control) {
2310
+ return sendError(
2311
+ res,
2312
+ 503,
2313
+ "no-last-session-store",
2314
+ t("web.last_session_unavailable", {}, lang)
2315
+ );
2316
+ }
2317
+ const body = await readJsonBody(req, void 0, lang);
2318
+ if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
2319
+ if (!conformsToWire(body.value, WIRE_LAST_SESSION_FIELDS)) {
2320
+ return sendError(res, 400, "bad-request", t("web.last_session_bad_request", {}, lang));
2321
+ }
2322
+ const { sessionId } = body.value;
2323
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
2324
+ return sendError(res, 400, "bad-request", t("web.last_session_bad_request", {}, lang));
2325
+ }
2326
+ if (!ctx.hub.has(sessionId) && !ctx.runtime.sessions?.get(sessionId)) {
2327
+ return sendUnknownSession(res, sessionId, lang);
2328
+ }
2329
+ const failure = control.set(sessionId);
2330
+ if (failure) return sendError(res, 500, "write-failed", failure(lang));
2331
+ sendJson(res, 200, { ok: true });
2332
+ }
2287
2333
  function readConfig(ctx, res, lang) {
2288
2334
  if (ctx.lanExposed) return sendLanExposed(res, lang);
2289
2335
  const outcome = ctx.runtime.mcp.readConfig();
@@ -2959,6 +3005,8 @@ function sendUploadPreview(res, outcome) {
2959
3005
  ok: false,
2960
3006
  plan: null,
2961
3007
  token: null,
3008
+ prefill: null,
3009
+ fromManifest: null,
2962
3010
  reason: outcome.reason,
2963
3011
  detail: outcome.detail
2964
3012
  });
@@ -2977,6 +3025,8 @@ function sendUploadPreview(res, outcome) {
2977
3025
  skippedLinks: [...skippedLinks]
2978
3026
  },
2979
3027
  token: outcome.token,
3028
+ prefill: outcome.prefill,
3029
+ fromManifest: [...outcome.fromManifest],
2980
3030
  reason: null,
2981
3031
  detail: null
2982
3032
  });
@@ -3361,6 +3411,12 @@ function listPending(ctx, res, lang) {
3361
3411
  const items = collectPendingApprovals(control.list(), control.recentRuns(RECENT_RUNS_LIMIT));
3362
3412
  sendJson(res, 200, { items });
3363
3413
  }
3414
+ function listRunning(ctx, res, lang) {
3415
+ const control = schedulesOf(ctx, res, lang);
3416
+ if (!control) return;
3417
+ const items = control.running().map(({ scheduleId, startedAt }) => ({ scheduleId, startedAt }));
3418
+ sendJson(res, 200, { items });
3419
+ }
3364
3420
  function getSchedule(ctx, res, id, lang) {
3365
3421
  const control = schedulesOf(ctx, res, lang);
3366
3422
  if (!control) return;
@@ -3394,10 +3450,20 @@ async function patchSchedule(ctx, req, res, id, lang) {
3394
3450
  if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
3395
3451
  const parsed = parseUpdate2(body.value, lang);
3396
3452
  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));
3453
+ const { enabled, ...rest } = parsed.value;
3454
+ const saved = Object.keys(rest).length > 0 ? await control.update(id, rest, { bypassAcknowledged: bypassAcknowledged(body.value) }) : { issues: [] };
3455
+ if (saved.issues.length > 0) return sendJson(res, 200, toSaveResponse(control, saved));
3456
+ const registration = enabled === void 0 ? saved.registration : await control.setEnabled(id, enabled);
3457
+ const next = control.get(id) ?? saved.schedule;
3458
+ sendJson(
3459
+ res,
3460
+ 200,
3461
+ toSaveResponse(control, {
3462
+ ...next ? { schedule: next } : {},
3463
+ issues: [],
3464
+ ...registration ? { registration } : {}
3465
+ })
3466
+ );
3401
3467
  }
3402
3468
  async function deleteSchedule(ctx, res, id, lang) {
3403
3469
  const control = schedulesOf(ctx, res, lang);
@@ -4738,6 +4804,9 @@ async function handleApi(opts, req, res, method, segments, lang) {
4738
4804
  return config(ctx, res, lang);
4739
4805
  }
4740
4806
  if (resource === "events" && method === "GET") return openStream(opts, req, res);
4807
+ if (resource === "last-session" && first === void 0 && method === "PUT") {
4808
+ return putLastSession(ctx, req, res, lang);
4809
+ }
4741
4810
  if (resource === "sessions") {
4742
4811
  return handleSessions(opts, req, res, method, first, second, detail, lang);
4743
4812
  }
@@ -4865,6 +4934,10 @@ async function handleSchedules(ctx, req, res, method, scheduleId, action, runId,
4865
4934
  if (isRead(method)) return listPending(ctx, res, lang);
4866
4935
  return methodNotAllowed(res, "GET", lang);
4867
4936
  }
4937
+ if (action === void 0 && scheduleId === "running") {
4938
+ if (isRead(method)) return listRunning(ctx, res, lang);
4939
+ return methodNotAllowed(res, "GET", lang);
4940
+ }
4868
4941
  if (action === void 0) {
4869
4942
  if (isRead(method)) return getSchedule(ctx, res, scheduleId, lang);
4870
4943
  if (method === "PATCH") return patchSchedule(ctx, req, res, scheduleId, lang);
@@ -4888,7 +4961,9 @@ async function handleSchedules(ctx, req, res, method, scheduleId, action, runId,
4888
4961
  async function handleSessions(opts, req, res, method, sessionId, action, detail, lang) {
4889
4962
  const { ctx } = opts;
4890
4963
  if (sessionId === void 0) {
4891
- if (isRead(method)) return listSessions(ctx, res, searchQuery(req.url));
4964
+ if (isRead(method)) {
4965
+ return listSessions(ctx, res, searchQuery(req.url), lang, archivedFlag(req.url));
4966
+ }
4892
4967
  if (method === "POST") return createSession(ctx, req, res, lang);
4893
4968
  return methodNotAllowed(res, "GET, POST", lang);
4894
4969
  }
@@ -5025,6 +5100,10 @@ function searchQuery(rawUrl) {
5025
5100
  const value = new URL(rawUrl ?? "/", "http://127.0.0.1").searchParams.get("q")?.trim();
5026
5101
  return value ? value : void 0;
5027
5102
  }
5103
+ function archivedFlag(rawUrl) {
5104
+ const raw = new URL(rawUrl ?? "/", "http://127.0.0.1").searchParams.get("archived");
5105
+ return raw === "1" || raw === "true";
5106
+ }
5028
5107
  function redirectWithoutToken(res, rawUrl) {
5029
5108
  const url = new URL(rawUrl ?? "/", "http://127.0.0.1");
5030
5109
  url.searchParams.delete(WIRE_AUTH_TOKEN_PARAM);