@code-yeongyu/senpi-agent-core 2026.10.21-2 → 2026.10.23-1

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.
@@ -88,7 +88,7 @@ class StreamIdleTimeoutError extends Error {
88
88
  // retried instead of dead-ending the session.
89
89
  export class StreamStartTimeoutError extends Error {
90
90
  constructor(timeoutMs) {
91
- super(`Provider stream start timed out after ${timeoutMs}ms (raise streamStartTimeoutMs — retry.provider.streamStartTimeoutMs in senpi settings; 0 disables)`);
91
+ super(`Provider stream start timed out after ${timeoutMs}ms (raise streamStartTimeoutMs: retry.provider.streamStartTimeoutMs in senpi settings; 0 disables)`);
92
92
  this.name = "StreamStartTimeoutError";
93
93
  }
94
94
  }
package/dist/agent.d.ts CHANGED
@@ -155,8 +155,10 @@ export declare class Agent {
155
155
  clearAllQueues(): void;
156
156
  /** Returns true when either queue still contains pending messages. */
157
157
  hasQueuedMessages(): boolean;
158
- /** Preview the messages selected for the next turn without consuming them. */
159
- peekQueuedMessages(): AgentMessage[];
158
+ /** Preview next-turn messages, or both queues with { all: true }, without consuming them. */
159
+ peekQueuedMessages(options?: {
160
+ all?: boolean;
161
+ }): AgentMessage[];
160
162
  /** Active abort signal for the current run, if any. */
161
163
  get signal(): AbortSignal | undefined;
162
164
  /** Abort the current run, if one is active. */
package/dist/agent.js CHANGED
@@ -236,9 +236,11 @@ export class Agent {
236
236
  hasQueuedMessages() {
237
237
  return this.steeringQueue.hasItems() || this.followUpQueue.hasItems();
238
238
  }
239
- /** Preview the messages selected for the next turn without consuming them. */
240
- peekQueuedMessages() {
239
+ /** Preview next-turn messages, or both queues with { all: true }, without consuming them. */
240
+ peekQueuedMessages(options) {
241
241
  const steering = this.steeringQueue.peek();
242
+ if (options?.all)
243
+ return [...steering, ...this.followUpQueue.peek()];
242
244
  return steering.length > 0 ? steering : this.followUpQueue.peek();
243
245
  }
244
246
  /** Active abort signal for the current run, if any. */
@@ -79,6 +79,9 @@ export const DEFAULT_COMPACTION_SETTINGS = {
79
79
  };
80
80
  /** Calculate total context tokens from provider usage. */
81
81
  export function calculateContextTokens(usage) {
82
+ if (usage.contextUsage !== undefined) {
83
+ return usage.contextUsage === null ? 0 : usage.contextUsage.input + usage.contextUsage.output;
84
+ }
82
85
  return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
83
86
  }
84
87
  function getAssistantUsage(msg) {
@@ -96,7 +96,11 @@ export declare class Harness<Ks extends readonly AnyKind[] = []> {
96
96
  private reconcileOrphans;
97
97
  quiescent(): boolean;
98
98
  hold(): () => void;
99
- /** Cancel and join in-process invocations, clear transient waits, then close without terminalizing tasks. */
99
+ /**
100
+ * Cancel and join in-process invocations, clear transient waits, then close without terminalizing tasks.
101
+ * The views and the session are closed even when joining or the waiting-state commit fails (a storage
102
+ * that refuses writes), so a failed suspend never leaks the session files; the first error is rethrown.
103
+ */
100
104
  suspend(ctx: Context): Promise<void>;
101
105
  registerTaskKind<K extends AnyKind>(kind: K): () => void;
102
106
  namespace<T extends JsonObject>(id: string, defaults: NamespaceDefaults<T>, opts?: {
@@ -266,26 +266,43 @@ export class Harness {
266
266
  throw new Error("cannot hold a non-quiescent harness; suspend it instead");
267
267
  return this.scheduler.hold();
268
268
  }
269
- /** Cancel and join in-process invocations, clear transient waits, then close without terminalizing tasks. */
269
+ /**
270
+ * Cancel and join in-process invocations, clear transient waits, then close without terminalizing tasks.
271
+ * The views and the session are closed even when joining or the waiting-state commit fails (a storage
272
+ * that refuses writes), so a failed suspend never leaks the session files; the first error is rethrown.
273
+ */
270
274
  async suspend(ctx) {
271
275
  if (this.suspended)
272
276
  return;
273
277
  this.suspended = true;
274
- await this.scheduler.joinAll();
275
- const tools = [...this.session.liveTasks.values()].filter((task) => task.kind === "pi.tool");
276
- if (tools.length > 0) {
277
- const docs = [...new Set(tools.map((task) => task.conversationId))].map((conversationId) => ({ doc: "sticky", conversationId }));
278
- await this.session.commit(KERNEL, (tx) => {
279
- for (const task of tools) {
280
- const index = task.input.index;
281
- const slot = tx.sticky(task.conversationId).turn.tools[index];
282
- if (slot?.waitingOn !== undefined)
283
- delete slot.waitingOn;
284
- }
285
- }, ctx, { docs });
278
+ let failure;
279
+ try {
280
+ await this.scheduler.joinAll();
281
+ const tools = [...this.session.liveTasks.values()].filter((task) => task.kind === "pi.tool");
282
+ if (tools.length > 0) {
283
+ const docs = [...new Set(tools.map((task) => task.conversationId))].map((conversationId) => ({ doc: "sticky", conversationId }));
284
+ await this.session.commit(KERNEL, (tx) => {
285
+ for (const task of tools) {
286
+ const index = task.input.index;
287
+ const slot = tx.sticky(task.conversationId).turn.tools[index];
288
+ if (slot?.waitingOn !== undefined)
289
+ delete slot.waitingOn;
290
+ }
291
+ }, ctx, { docs });
292
+ }
293
+ }
294
+ catch (error) {
295
+ failure = { error };
286
296
  }
287
297
  this.views.close();
288
- await this.session.close(ctx);
298
+ try {
299
+ await this.session.close(ctx);
300
+ }
301
+ catch (error) {
302
+ failure ??= { error };
303
+ }
304
+ if (failure !== undefined)
305
+ throw failure.error;
289
306
  }
290
307
  // --- registries --------------------------------------------------------------
291
308
  registerTaskKind(kind) {
@@ -317,6 +334,7 @@ export class Harness {
317
334
  routes.set(key, doc);
318
335
  }
319
336
  }
337
+ // oxlint-disable-next-line eslint/prefer-const -- a closure reads it; const would throw in the TDZ if that closure ran before the assignment, so the rewrite is reviewed separately
320
338
  let token;
321
339
  token = {
322
340
  id,
@@ -28,6 +28,7 @@ export function addUsage(left, right) {
28
28
  cacheWrite: left.cost.cacheWrite + right.cost.cacheWrite,
29
29
  total: left.cost.total + right.cost.total,
30
30
  },
31
+ ...(left.costUnknown || right.costUnknown ? { costUnknown: true } : {}),
31
32
  };
32
33
  }
33
34
  //# sourceMappingURL=usage.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-agent-core",
3
- "version": "2026.10.21-2",
3
+ "version": "2026.10.23-1",
4
4
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -62,8 +62,8 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "@earendil-works/chord": "0.99.1",
65
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.21-2",
66
- "@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.21-2",
65
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.23-1",
66
+ "@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.23-1",
67
67
  "diff": "9.0.0",
68
68
  "ignore": "7.0.9",
69
69
  "typebox": "1.3.34",