neoctl 0.2.49 → 0.2.51

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.
@@ -60,6 +60,9 @@ export type AgentEvent = {
60
60
  } | {
61
61
  type: "context.metrics";
62
62
  metrics: ContextMetrics;
63
+ } | {
64
+ type: "context.compaction.delta";
65
+ text: string;
63
66
  } | {
64
67
  type: "context.compacted";
65
68
  compaction: CompactionReport;
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { type SessionPageOptions } from "../session/session-pagination.js";
2
3
  import { type ToolInputChange } from "../model/tool-input-stream.js";
3
4
  import { type ToolActivity } from "./tool-activity.js";
4
5
  import type { TimingRecord } from "../core/query-timing.js";
@@ -210,6 +211,7 @@ type WebActionResult<T extends object = object> = ({
210
211
  ok: true;
211
212
  } & T) | WebActionFailure;
212
213
  interface UiStatus {
214
+ compactionText?: string;
213
215
  modelOutput?: {
214
216
  kind: "text";
215
217
  } | {
@@ -372,6 +374,7 @@ export declare class WebRuntimeRouter {
372
374
  constructor(options?: WebRuntimeRouterOptions);
373
375
  get(scope?: WebRuntimeScope): Promise<WebRepl>;
374
376
  snapshot(scope?: WebRuntimeScope, includeCatalog?: boolean): Promise<ReturnType<WebRepl["snapshot"]>>;
377
+ runningSessionIds(): Promise<string[]>;
375
378
  reloadModelConfig(): Promise<void>;
376
379
  reloadGlobalTools(overrides: Record<string, boolean>): Promise<void>;
377
380
  activeScopes(): string[];
@@ -467,6 +470,11 @@ export declare class WebRepl {
467
470
  sessionId: string;
468
471
  runId: string;
469
472
  } | undefined;
473
+ runningSessionIds(): string[];
474
+ queuedImage(imageId: string): {
475
+ body: Buffer;
476
+ mimeType: string;
477
+ } | undefined;
470
478
  snapshot(includeCatalog?: boolean): {
471
479
  lines: UiLine[];
472
480
  status: UiStatus;
@@ -483,6 +491,17 @@ export declare class WebRepl {
483
491
  };
484
492
  demoMode: boolean;
485
493
  queuedInput: string | undefined;
494
+ queuedAttachments: ({
495
+ kind: "image";
496
+ imageId: string;
497
+ previewUrl: string;
498
+ name?: undefined;
499
+ } | {
500
+ kind: "file";
501
+ name: string;
502
+ imageId?: undefined;
503
+ previewUrl?: undefined;
504
+ })[];
486
505
  backgroundTaskCount: number;
487
506
  backgroundTasks: ({
488
507
  kind: "agent";
@@ -800,9 +819,17 @@ export declare class WebRepl {
800
819
  }>;
801
820
  private enqueueInput;
802
821
  private startRun;
803
- listSessions(): Promise<{
822
+ listSessions(options?: SessionPageOptions): Promise<{
823
+ runningSessionIds: string[];
804
824
  sessions: import("../index.js").SessionSummary[];
825
+ total: number;
826
+ totalCount: number;
827
+ page: number;
828
+ pageSize: number;
829
+ totalPages: number;
830
+ } | {
805
831
  runningSessionIds: string[];
832
+ sessions: import("../index.js").SessionSummary[];
806
833
  }>;
807
834
  resumeSession(sessionId: string): Promise<WebActionResult>;
808
835
  newSession(): Promise<WebActionResult>;
package/dist/web/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { paginateSessions, sessionPageOptions } from "../session/session-pagination.js";
2
3
  import { applyToolInputChanges } from "../model/tool-input-stream.js";
3
4
  import { toolActivity } from "./tool-activity.js";
4
5
  import { toolObservationResult } from "./tool-observation-result.js";
@@ -498,6 +499,11 @@ export class WebRuntimeRouter {
498
499
  async snapshot(scope = {}, includeCatalog = true) {
499
500
  return (await this.get(scope)).snapshot(includeCatalog);
500
501
  }
502
+ async runningSessionIds() {
503
+ // Inspect only existing runtime instances: never restore sessions or read transcripts.
504
+ const entries = await Promise.allSettled([...this.repls.values()]);
505
+ return [...new Set(entries.flatMap(entry => entry.status === "fulfilled" ? entry.value.runningSessionIds() : []))];
506
+ }
501
507
  async reloadModelConfig() {
502
508
  const repls = await Promise.all([...this.repls.values()]);
503
509
  await Promise.all(repls.map(repl => repl.reloadModelConfig()));
@@ -749,6 +755,14 @@ export class WebRepl {
749
755
  // Cursor addresses stored bytes, not the possibly additionally redacted response length.
750
756
  return page.ok ? this.runtime.engine.redactDisplayValue({ sessionId, runId, ...page.value }) : undefined;
751
757
  }
758
+ runningSessionIds() {
759
+ const id = this.runtime.engine.snapshot().session?.sessionId;
760
+ return [...new Set([...this.backgroundSessionRuns.keys(), ...(this.busy && !this.runControl().canResume && id ? [id] : [])])];
761
+ }
762
+ queuedImage(imageId) {
763
+ const attachment = this.queuedAttachments?.find(item => item.kind === "image" && item.imageId === imageId);
764
+ return attachment?.kind === "image" ? imagePayloadForBlock({ type: "image", ...attachment }) : undefined;
765
+ }
752
766
  snapshot(includeCatalog = false) {
753
767
  const backgroundTasks = this.backgroundTasks();
754
768
  const engineSnapshot = this.runtime.engine.snapshot();
@@ -765,12 +779,16 @@ export class WebRepl {
765
779
  runControl: this.runControl(),
766
780
  demoMode: this.isDemo(),
767
781
  queuedInput: this.queuedInput,
782
+ // Preview metadata only. Base64 image contents must not inflate every status snapshot.
783
+ queuedAttachments: (this.queuedAttachments ?? []).map(item => item.kind === "image"
784
+ ? { kind: "image", imageId: item.imageId, previewUrl: `/api/queue/images/${encodeURIComponent(item.imageId)}` }
785
+ : { kind: "file", name: item.name }),
768
786
  backgroundTaskCount: backgroundTasks.length,
769
787
  backgroundTasks,
770
788
  terminalTaskHistory: this.terminalTaskHistory(),
771
789
  agentTaskHistory: sessionAgentTasks(this.runtime).filter((task) => this.runtime.taskStore.isTerminal(task)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).map((task) => this.agentTaskSnapshot(task)),
772
790
  backgroundSessionRunCount: this.backgroundSessionRuns.size,
773
- runningSessionIds: [...this.backgroundSessionRuns.keys()],
791
+ runningSessionIds: this.runningSessionIds(),
774
792
  session: engineSnapshot.session,
775
793
  modelSettings: { model: engineSnapshot.model, reasoning: engineSnapshot.reasoning },
776
794
  pendingSessionSettings,
@@ -1097,10 +1115,10 @@ export class WebRepl {
1097
1115
  this.foregroundRun = undefined;
1098
1116
  }).catch(() => undefined);
1099
1117
  }
1100
- async listSessions() {
1118
+ async listSessions(options) {
1101
1119
  const sessions = await this.runtime.engine.listSessions(Number.POSITIVE_INFINITY);
1102
- const runningSessionIds = [...this.backgroundSessionRuns.keys()];
1103
- return { sessions, runningSessionIds };
1120
+ const runningSessionIds = this.runningSessionIds();
1121
+ return { ...(options ? paginateSessions(sessions, options) : { sessions }), runningSessionIds };
1104
1122
  }
1105
1123
  async resumeSession(sessionId) {
1106
1124
  if (!sessionId)
@@ -1347,11 +1365,15 @@ export class WebRepl {
1347
1365
  if (showProgress) {
1348
1366
  this.compactingSession = engine;
1349
1367
  this.busy = true;
1350
- this.setStatus({ ...this.status, phase: "compacting", detail: "manualcompact: generating context summary", activityTick: this.status.activityTick + 1 });
1368
+ this.setStatus({ ...this.status, phase: "compacting", compactionText: "", detail: "manualcompact: generating context summary", activityTick: this.status.activityTick + 1 });
1351
1369
  }
1352
1370
  try {
1353
1371
  // An idle click executes now through the settings barrier, not the chat/input queue.
1354
- const result = await engine.updateSessionSettings({ compact: true }, { abortSignal: controller?.signal });
1372
+ const result = await engine.updateSessionSettings({ compact: true }, { abortSignal: controller?.signal, onCompactionDelta: text => {
1373
+ if (showProgress && this.runtime.engine === engine && this.foregroundRunToken === runToken && !controller?.signal.aborted) {
1374
+ this.handleEvent({ type: "context.compaction.delta", text });
1375
+ }
1376
+ } });
1355
1377
  if (this.runtime.engine === engine && this.foregroundRunToken === runToken) {
1356
1378
  if (result.compaction?.report)
1357
1379
  this.appendCompaction(result.compaction.report);
@@ -1545,6 +1567,8 @@ export class WebRepl {
1545
1567
  this.broadcastSync();
1546
1568
  }
1547
1569
  setStatus(next) {
1570
+ if (next.phase !== "compacting")
1571
+ next = { ...next, compactionText: undefined };
1548
1572
  const waiting = next.phase === "calling_model" && !next.modelOutput;
1549
1573
  const wasWaiting = this.status.phase === "calling_model" && !this.status.modelOutput;
1550
1574
  next = { ...next, responseWaitStartedAt: waiting
@@ -2002,6 +2026,10 @@ export class WebRepl {
2002
2026
  this.liveToolLineIds.set(event.toolUse.id, this.append(patch));
2003
2027
  return;
2004
2028
  }
2029
+ if (event.type === "context.compaction.delta") {
2030
+ this.queueDeltaStatus();
2031
+ return;
2032
+ }
2005
2033
  if (event.type === "tool_call.delta") {
2006
2034
  this.queueDeltaStatus();
2007
2035
  return;
@@ -2336,10 +2364,13 @@ export class WebRepl {
2336
2364
  this.activeAbortController = abortController;
2337
2365
  this.interruptArmed = false;
2338
2366
  this.setBusy(true);
2339
- this.setStatus({ ...this.status, phase: "compacting", detail: type === "compact" ? "manual compact" : "pure compact", activityTick: this.status.activityTick + 1 });
2367
+ this.setStatus({ ...this.status, phase: "compacting", compactionText: "", detail: type === "compact" ? "manual compact" : "pure compact", activityTick: this.status.activityTick + 1 });
2340
2368
  try {
2341
2369
  const result = type === "compact"
2342
- ? await this.runtime.engine.compact({ abortSignal: abortController.signal })
2370
+ ? await this.runtime.engine.compact({ abortSignal: abortController.signal, onCompactionDelta: text => {
2371
+ if (this.foregroundRunToken === runToken && !abortController.signal.aborted)
2372
+ this.handleEvent({ type: "context.compaction.delta", text });
2373
+ } })
2343
2374
  : await this.runtime.engine.pureCompact({ abortSignal: abortController.signal });
2344
2375
  if (this.foregroundRunToken !== runToken)
2345
2376
  return;
@@ -2556,6 +2587,8 @@ export async function handleWebRequest(req, res, router) {
2556
2587
  if (["/api/tool-call-detail", "/api/terminal-output", "/api/agent-content"].includes(url.pathname) && !allowToolDetailRequest(req.headers.origin, req.headers.host, req.headers["sec-fetch-site"])) {
2557
2588
  return sendJson(res, { error: "详情访问被拒绝" }, 403);
2558
2589
  }
2590
+ if (req.method === "GET" && url.pathname === "/api/sessions/running")
2591
+ return sendJson(res, { runningSessionIds: await router.runningSessionIds() });
2559
2592
  const scope = webRuntimeScopeFromUrl(url);
2560
2593
  const repl = await router.get(scope);
2561
2594
  if (req.method === "GET" && url.pathname === "/events")
@@ -2585,6 +2618,11 @@ export async function handleWebRequest(req, res, router) {
2585
2618
  const page = repl.terminalOutput(url.searchParams.get("sessionId") ?? "", url.searchParams.get("runId") ?? "", url.searchParams.get("stream") ?? "stdout", Number(url.searchParams.get("offset") ?? 0), Number(url.searchParams.get("limitBytes") ?? 65536), url.searchParams.get("tail") === "true");
2586
2619
  return page ? sendJson(res, page) : sendJson(res, { error: "终端输出不可获取或无权访问" }, 404);
2587
2620
  }
2621
+ const queuedImageMatch = /^\/api\/queue\/images\/(image_[0-9a-f-]{36})$/iu.exec(url.pathname);
2622
+ if (req.method === "GET" && queuedImageMatch) {
2623
+ const image = repl.queuedImage(queuedImageMatch[1]);
2624
+ return image ? sendImage(res, image, req) : sendJson(res, { error: "图片不可用" }, 404);
2625
+ }
2588
2626
  if (req.method === "GET" && url.pathname === "/api/state")
2589
2627
  return sendJson(res, repl.snapshot(true));
2590
2628
  if (req.method === "GET" && url.pathname === "/api/runtime-context")
@@ -2755,7 +2793,7 @@ export async function handleWebRequest(req, res, router) {
2755
2793
  if (req.method === "POST" && url.pathname === "/api/queue/send-now")
2756
2794
  return sendJson(res, await repl.sendQueuedNow());
2757
2795
  if (req.method === "GET" && url.pathname === "/api/sessions")
2758
- return sendJson(res, await repl.listSessions());
2796
+ return sendJson(res, await repl.listSessions(sessionPageOptions(url.searchParams)));
2759
2797
  if (req.method === "POST" && url.pathname === "/api/sessions/resume") {
2760
2798
  const body = await readJsonBody(req);
2761
2799
  return sendJson(res, await repl.resumeSession(String(body.sessionId ?? "")));
@@ -3115,8 +3153,10 @@ function toolStreamStepStatus(phase) {
3115
3153
  return "running";
3116
3154
  }
3117
3155
  function reduceStatus(status, event) {
3156
+ if (event.type === "context.compaction.delta")
3157
+ return { ...status, phase: "compacting", compactionText: (status.compactionText || "") + event.text, activityTick: status.activityTick + 1 };
3118
3158
  if (event.type === "state")
3119
- return { ...status, phase: event.phase, detail: event.detail, modelOutput: undefined, currentTool: event.phase === "running_tools" ? status.currentTool : undefined, usage: event.phase === "preparing" ? undefined : status.usage, streamedOutputTokens: event.phase === "preparing" ? 0 : status.streamedOutputTokens, inputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.inputTokenUpdatedAt, outputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.outputTokenUpdatedAt, retryCooldownUntil: event.phase === "preparing" ? undefined : status.retryCooldownUntil, activityTick: status.activityTick + 1 };
3159
+ return { ...status, phase: event.phase, detail: event.detail, compactionText: event.phase === "compacting" && status.phase === "compacting" ? status.compactionText : undefined, modelOutput: undefined, currentTool: event.phase === "running_tools" ? status.currentTool : undefined, usage: event.phase === "preparing" ? undefined : status.usage, streamedOutputTokens: event.phase === "preparing" ? 0 : status.streamedOutputTokens, inputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.inputTokenUpdatedAt, outputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.outputTokenUpdatedAt, retryCooldownUntil: event.phase === "preparing" ? undefined : status.retryCooldownUntil, activityTick: status.activityTick + 1 };
3120
3160
  if (event.type === "context.compacted")
3121
3161
  return { ...status, phase: "compacting", detail: formatCompactionReportSummary(event.compaction), activityTick: status.activityTick + 1 };
3122
3162
  if (event.type === "context.metrics")