@jpcasabiancai/cayenne 0.5.2 → 0.5.4

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.
Files changed (2) hide show
  1. package/dist/cli.js +170 -44
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -7753,7 +7753,15 @@ async function askGuard(guard, toolName2, input) {
7753
7753
  return GUARD_FAILED;
7754
7754
  }
7755
7755
  }
7756
- var SESSION_FLAG_SETTINGS, GUARD_FAILED, ASK_TOOL_REASON, ClaudeSession;
7756
+ function asInterrupted(row) {
7757
+ return {
7758
+ ...row,
7759
+ content: row.content.map(
7760
+ (block) => block.type === "result" ? { ...block, isError: false, text: INTERRUPTED } : block
7761
+ )
7762
+ };
7763
+ }
7764
+ var SESSION_FLAG_SETTINGS, GUARD_FAILED, ASK_TOOL_REASON, INTERRUPTED, STOPPED_TURN, ClaudeSession;
7757
7765
  var init_session2 = __esm({
7758
7766
  "src/claude/session.ts"() {
7759
7767
  "use strict";
@@ -7768,6 +7776,8 @@ var init_session2 = __esm({
7768
7776
  };
7769
7777
  GUARD_FAILED = "Refused: the runner could not check this tool call.";
7770
7778
  ASK_TOOL_REASON = "This session was started from a task, so every shell command needs the member's approval.";
7779
+ INTERRUPTED = "interrupted";
7780
+ STOPPED_TURN = "error_during_execution after a requested stop: recorded as interrupted";
7771
7781
  ClaudeSession = class extends EventEmitter {
7772
7782
  constructor(opts) {
7773
7783
  super();
@@ -7786,6 +7796,13 @@ var init_session2 = __esm({
7786
7796
  /** `null` until `accountInfo()` answers, and for good when it cannot. */
7787
7797
  authMode = null;
7788
7798
  authAsked = false;
7799
+ /** A prompt is waiting for its turn, or a turn began (`init`), and no result has closed it. */
7800
+ turnOpen = false;
7801
+ /**
7802
+ * `interrupt()` or `stop()` was asked of the open turn. The CLI reports a turn cut short as
7803
+ * `error_during_execution`; with this set, that result is the stop landing, not a failure.
7804
+ */
7805
+ stopAsked = false;
7789
7806
  start() {
7790
7807
  if (this.q) throw new Error("VALIDATION: session already started");
7791
7808
  const queryFn = this.opts.queryFn ?? sdkQuery;
@@ -7869,6 +7886,7 @@ var init_session2 = __esm({
7869
7886
  })),
7870
7887
  { type: "text", text }
7871
7888
  ] : text;
7889
+ this.turnOpen = true;
7872
7890
  this.inbox.push({
7873
7891
  type: "user",
7874
7892
  message: { role: "user", content },
@@ -7885,6 +7903,7 @@ var init_session2 = __esm({
7885
7903
  return true;
7886
7904
  }
7887
7905
  async interrupt() {
7906
+ if (this.turnOpen) this.stopAsked = true;
7888
7907
  await this.q?.interrupt();
7889
7908
  }
7890
7909
  async setPermissionMode(mode) {
@@ -7901,6 +7920,7 @@ var init_session2 = __esm({
7901
7920
  for (const id of [...this.pending.keys()]) {
7902
7921
  this.expire(id, "session stopped");
7903
7922
  }
7923
+ if (this.turnOpen) this.stopAsked = true;
7904
7924
  void this.q?.interrupt().catch(() => void 0);
7905
7925
  const killer = setTimeout(() => this.abort.abort(), 5e3);
7906
7926
  await this.pumping;
@@ -7955,6 +7975,7 @@ ${this.stderrTail.join("\n")}` : "";
7955
7975
  }
7956
7976
  route(msg) {
7957
7977
  if (msg.type === "system" && msg.subtype === "init") {
7978
+ this.turnOpen = true;
7958
7979
  this.providerSessionId = msg.session_id;
7959
7980
  this.lookUpAuthMode();
7960
7981
  this.emit("init", {
@@ -7974,15 +7995,19 @@ ${this.stderrTail.join("\n")}` : "";
7974
7995
  });
7975
7996
  return;
7976
7997
  }
7998
+ const stopped = msg.type === "result" && this.stopAsked && msg.subtype === "error_during_execution";
7999
+ if (stopped) logger.info({ providerSessionId: this.providerSessionId }, STOPPED_TURN);
7977
8000
  const normalized = normalizeSdkMessage(msg);
7978
- if (normalized) this.emit("message", normalized);
8001
+ if (normalized) this.emit("message", stopped ? asInterrupted(normalized) : normalized);
7979
8002
  if (msg.type === "result") {
8003
+ this.turnOpen = false;
8004
+ this.stopAsked = false;
7980
8005
  this.emit("result", {
7981
- isError: msg.is_error,
8006
+ isError: stopped ? false : msg.is_error,
7982
8007
  durationMs: msg.duration_ms,
7983
8008
  numTurns: msg.num_turns,
7984
8009
  totalCostUsd: msg.total_cost_usd,
7985
- stopReason: msg.stop_reason,
8010
+ stopReason: stopped ? INTERRUPTED : msg.stop_reason,
7986
8011
  authMode: this.authMode
7987
8012
  });
7988
8013
  }
@@ -9925,6 +9950,9 @@ function worthRespawning(current, next) {
9925
9950
  if (next.state !== "ok") return next.state === "none" && current.state !== "none";
9926
9951
  return current.state !== "ok" || current.token !== next.token;
9927
9952
  }
9953
+ function refusedByPostgres(code) {
9954
+ return typeof code === "string" && /^[0-9A-Z]{5}$/.test(code) && code !== UNIQUE_VIOLATION && code !== "40003" && !code.startsWith("08") && !code.startsWith("57P");
9955
+ }
9928
9956
  function assertTaskMode(mode) {
9929
9957
  if (TASK_REFUSED_MODES.has(mode)) {
9930
9958
  throw new Error(`VALIDATION: a task session cannot run in ${mode}`);
@@ -9962,7 +9990,7 @@ function shrink(event) {
9962
9990
  return null;
9963
9991
  }
9964
9992
  }
9965
- var READ_ONLY_TEXT, REFRESH_WITHIN_MS, betweenTurns, UNREADABLE_MARK_CREDS, roundUsd, ORPHANED_STATUSES, RESTARTABLE_STATUSES, isAtOrBelow3, DENY_MESSAGE_MAX, EXIT_ERROR_MAX, INITIAL_PROMPT_MAX, TASK_SKILL_LOOKUP_MS, TASK_REFUSED_MODES, OWNER_DRIVEN_KINDS, REPO_OWNER_ONLY, REPO_NO_DUPLICATE, PROVIDER_SESSION_ID, REPO_WORKTREE_GONE, TASK_ASK_TOOLS, MEMORY_PREAMBLE, MEMORY_SOURCE, wrapMemory, PATH_INPUT_TOOLS, PATH_INPUT_KEYS, isRecord, errorCode, SessionManager;
9993
+ var READ_ONLY_TEXT, REFRESH_WITHIN_MS, betweenTurns, UNREADABLE_MARK_CREDS, DRAINED_AFTER_MS, roundUsd, UNIQUE_VIOLATION, ROW_WRITE_TIMEOUT_MS, ORPHANED_STATUSES, RESTARTABLE_STATUSES, isAtOrBelow3, DENY_MESSAGE_MAX, EXIT_ERROR_MAX, INITIAL_PROMPT_MAX, TASK_SKILL_LOOKUP_MS, TASK_REFUSED_MODES, OWNER_DRIVEN_KINDS, REPO_OWNER_ONLY, REPO_NO_DUPLICATE, PROVIDER_SESSION_ID, REPO_WORKTREE_GONE, TASK_ASK_TOOLS, MEMORY_PREAMBLE, MEMORY_SOURCE, wrapMemory, PATH_INPUT_TOOLS, PATH_INPUT_KEYS, isRecord, errorCode, SessionManager;
9966
9994
  var init_manager = __esm({
9967
9995
  "src/sessions/manager.ts"() {
9968
9996
  "use strict";
@@ -10003,7 +10031,10 @@ var init_manager = __esm({
10003
10031
  state: "unavailable",
10004
10032
  readOnlyText: READ_ONLY_UNAVAILABLE
10005
10033
  };
10034
+ DRAINED_AFTER_MS = 2e3;
10006
10035
  roundUsd = (usd) => Math.round(usd * 1e6) / 1e6;
10036
+ UNIQUE_VIOLATION = "23505";
10037
+ ROW_WRITE_TIMEOUT_MS = 3e4;
10007
10038
  ORPHANED_STATUSES = ["running", "awaiting_permission"];
10008
10039
  RESTARTABLE_STATUSES = /* @__PURE__ */ new Set(["idle", ...ORPHANED_STATUSES]);
10009
10040
  isAtOrBelow3 = (p, dir) => p === dir || p.startsWith(dir.endsWith(sep16) ? dir : `${dir}${sep16}`);
@@ -10604,6 +10635,7 @@ var init_manager = __esm({
10604
10635
  );
10605
10636
  await this.setStatus(sessionId, "running");
10606
10637
  live.session.prompt(text, p.clientUuid, staged);
10638
+ this.turnHandedOver(live);
10607
10639
  this.deps.timings?.prompted(sessionId);
10608
10640
  } catch (err) {
10609
10641
  staged.discard();
@@ -10966,12 +10998,78 @@ var init_manager = __esm({
10966
10998
  async interrupt(sessionId) {
10967
10999
  const live = this.live.get(sessionId);
10968
11000
  if (live) {
10969
- await this.provider(() => live.session.interrupt());
11001
+ const { turns } = live;
11002
+ turns.stops = turns.owed;
11003
+ turns.chased = false;
11004
+ turns.early = turns.owed > 0 && !turns.begun && !live.stopping && !live.dropping;
11005
+ if (turns.early) {
11006
+ this.sendStop(sessionId, live);
11007
+ if (live.status === "running") await this.setStatus(sessionId, "idle");
11008
+ return {};
11009
+ }
11010
+ try {
11011
+ await this.provider(() => live.session.interrupt());
11012
+ } catch (err) {
11013
+ turns.stops = 0;
11014
+ throw err;
11015
+ }
10970
11016
  return {};
10971
11017
  }
10972
11018
  await this.settleOrphans([sessionId]);
10973
11019
  return {};
10974
11020
  }
11021
+ /** A stop nobody waits on: a provider that is starting, or between turns, may answer late. */
11022
+ sendStop(sessionId, live) {
11023
+ void live.session.interrupt().catch(
11024
+ (err) => this.deps.log.warn(
11025
+ { err: err instanceof Error ? err.message : String(err), sessionId },
11026
+ "a stop sent ahead of its turn failed"
11027
+ )
11028
+ );
11029
+ }
11030
+ /** A prompt went to the provider. */
11031
+ turnHandedOver(live) {
11032
+ const { turns } = live;
11033
+ const leftover = live.provider === "claude" && turns.stops === 0 && turns.drainedAt !== null && Date.now() - turns.drainedAt > DRAINED_AFTER_MS;
11034
+ if (leftover) {
11035
+ turns.owed = 0;
11036
+ turns.stops = 0;
11037
+ }
11038
+ turns.drainedAt = null;
11039
+ turns.owed += 1;
11040
+ }
11041
+ /** The provider announced a turn (`init`): if a Stop covers it, this is where it lands. */
11042
+ turnBegan(sessionId, live) {
11043
+ if (live.provider !== "claude") return;
11044
+ const { turns } = live;
11045
+ turns.drainedAt = null;
11046
+ if (turns.owed > 0) turns.begun = true;
11047
+ if (turns.stops > 0 && turns.early) this.sendStop(sessionId, live);
11048
+ }
11049
+ /**
11050
+ * The provider produced output. For a turn stopped before it began that means the stop did not
11051
+ * land (a backend with no `init` per turn, or a stop that was lost): it is sent once more, and
11052
+ * `true` tells the caller the row must say `running` again, since a turn is.
11053
+ */
11054
+ turnOutput(sessionId, live) {
11055
+ const { turns } = live;
11056
+ turns.drainedAt = null;
11057
+ if (turns.owed > 0) turns.begun = true;
11058
+ if (turns.stops === 0 || !turns.early || turns.chased) return false;
11059
+ turns.chased = true;
11060
+ this.sendStop(sessionId, live);
11061
+ return live.status === "idle";
11062
+ }
11063
+ /** A turn ended: whatever a Stop still covers is behind it, and has not begun. */
11064
+ turnEnded(live) {
11065
+ const { turns } = live;
11066
+ turns.owed = Math.max(0, turns.owed - 1);
11067
+ turns.stops = Math.max(0, turns.stops - 1);
11068
+ turns.begun = false;
11069
+ turns.drainedAt = Date.now();
11070
+ turns.early = true;
11071
+ turns.chased = false;
11072
+ }
10975
11073
  /**
10976
11074
  * Put rows on this runner that claim a turn in progress back to `idle`, for the chats that have
10977
11075
  * no live session here — at boot every one of them, since nothing is live yet, and again when
@@ -11804,8 +11902,10 @@ var init_manager = __esm({
11804
11902
  permissions: /* @__PURE__ */ new Map(),
11805
11903
  cwd: opts.cwd,
11806
11904
  stopping: false,
11905
+ turns: { owed: 0, begun: false, drainedAt: null, stops: 0, early: false, chased: false },
11807
11906
  dropping: null,
11808
11907
  inflight: /* @__PURE__ */ new Set(),
11908
+ writes: Promise.resolve(),
11809
11909
  changesVersion: this.nextChangesSeed(),
11810
11910
  watcher: null,
11811
11911
  docs: null,
@@ -11969,9 +12069,12 @@ var init_manager = __esm({
11969
12069
  live.inflight.add(task);
11970
12070
  void task.finally(() => live.inflight.delete(task));
11971
12071
  };
11972
- session.on(
11973
- "init",
11974
- (init) => guard(async () => {
12072
+ const output = () => {
12073
+ if (this.turnOutput(sessionId, live)) guard(() => this.setStatus(sessionId, "running"))();
12074
+ };
12075
+ session.on("init", (init) => {
12076
+ this.turnBegan(sessionId, live);
12077
+ return guard(async () => {
11975
12078
  await this.updateSession(sessionId, {
11976
12079
  provider_session_id: init.providerSessionId,
11977
12080
  model: init.model
@@ -11982,9 +12085,10 @@ var init_manager = __esm({
11982
12085
  providerSessionId: init.providerSessionId,
11983
12086
  model: init.model
11984
12087
  });
11985
- })()
11986
- );
12088
+ })();
12089
+ });
11987
12090
  session.on("stream", (s) => {
12091
+ output();
11988
12092
  this.deps.timings?.streamed(sessionId);
11989
12093
  guard(
11990
12094
  () => this.broadcast(sessionId, {
@@ -11995,9 +12099,9 @@ var init_manager = __esm({
11995
12099
  })
11996
12100
  )();
11997
12101
  });
11998
- session.on(
11999
- "message",
12000
- (m) => guard(async () => {
12102
+ session.on("message", (m) => {
12103
+ if (m.role === "assistant" || m.role === "tool") output();
12104
+ return guard(async () => {
12001
12105
  if (m.role === "user" && m.providerUuid && live.echoed.has(m.providerUuid)) return;
12002
12106
  await this.persist(
12003
12107
  sessionId,
@@ -12008,8 +12112,8 @@ var init_manager = __esm({
12008
12112
  m.parentToolUseId,
12009
12113
  m.raw
12010
12114
  );
12011
- })()
12012
- );
12115
+ })();
12116
+ });
12013
12117
  session.on(
12014
12118
  "permission",
12015
12119
  (rawReq) => guard(async () => {
@@ -12098,14 +12202,17 @@ var init_manager = __esm({
12098
12202
  })()
12099
12203
  );
12100
12204
  session.on("result", (r) => {
12205
+ this.turnEnded(live);
12101
12206
  this.deps.timings?.finished(sessionId);
12207
+ const lastSeq = live.writes.then(() => live.seq);
12102
12208
  guard(async () => {
12103
12209
  if (live.provider === "claude") await this.recordCost(sessionId, live, r);
12210
+ const seq = await lastSeq;
12104
12211
  const status = r.isError ? "error" : "idle";
12105
12212
  live.status = status;
12106
12213
  await this.updateSession(sessionId, { status, last_message_at: (/* @__PURE__ */ new Date()).toISOString() });
12107
12214
  const { authMode: _authMode, ...event } = r;
12108
- await this.broadcast(sessionId, { type: "result", seq: live.seq, ...event });
12215
+ await this.broadcast(sessionId, { type: "result", seq, ...event });
12109
12216
  await this.broadcast(sessionId, {
12110
12217
  type: "session.status",
12111
12218
  status,
@@ -12213,36 +12320,55 @@ var init_manager = __esm({
12213
12320
  );
12214
12321
  });
12215
12322
  }
12323
+ /**
12324
+ * Write one `chat_messages` row and broadcast its `message` twin, in this session's write order.
12325
+ *
12326
+ * The step is queued on `live.writes` before this method first awaits, so two calls made back to
12327
+ * back (the SDK hands a short turn's reply and result row over on one tick) insert and broadcast
12328
+ * in call order: insert N and its broadcast both happen before insert N+1 starts. A caller on
12329
+ * the prompt path may therefore wait for earlier rows' inserts — a database wait. It never
12330
+ * waits on a relay send: `broadcast` returns once the relay has the event queued.
12331
+ *
12332
+ * Rejects with `PROVIDER_ERROR: persist failed: …` for this row only; the chain goes on.
12333
+ */
12216
12334
  async persist(sessionId, live, role, content, providerUuid, parentToolUseId, raw) {
12217
12335
  const safeContent = this.secrets.redact(content);
12218
12336
  const safeRaw = this.secrets.redact(raw);
12219
- live.seq += 1;
12220
- const seq = live.seq;
12221
- const { data, error } = await this.deps.db.from("chat_messages").insert({
12222
- session_id: sessionId,
12223
- // Overwritten from the session by stamp_message_workspace; the insert policy still needs
12224
- // is_member(workspace_id) to see a non-null value.
12225
- workspace_id: this.deps.workspaceId,
12226
- seq,
12227
- role,
12228
- content: safeContent,
12229
- provider_uuid: providerUuid,
12230
- parent_tool_use_id: parentToolUseId,
12231
- raw: safeRaw
12232
- }).select(
12233
- "id, session_id, workspace_id, seq, role, content, provider_uuid, parent_tool_use_id, created_at"
12234
- ).single();
12235
- if (error || !data) {
12236
- throw new Error(`PROVIDER_ERROR: persist failed: ${error?.message ?? "no row"}`);
12237
- }
12238
- const row = {
12239
- ...data,
12240
- seq,
12241
- role,
12242
- content: safeContent
12243
- };
12244
- await this.broadcast(sessionId, { type: "message", message: row });
12245
- return row;
12337
+ const step = live.writes.then(async () => {
12338
+ const seq = live.seq + 1;
12339
+ live.seq = seq;
12340
+ const { data, error } = await this.deps.db.from("chat_messages").insert({
12341
+ session_id: sessionId,
12342
+ // Overwritten from the session by stamp_message_workspace; the insert policy still needs
12343
+ // is_member(workspace_id) to see a non-null value.
12344
+ workspace_id: this.deps.workspaceId,
12345
+ seq,
12346
+ role,
12347
+ content: safeContent,
12348
+ provider_uuid: providerUuid,
12349
+ parent_tool_use_id: parentToolUseId,
12350
+ raw: safeRaw
12351
+ }).select(
12352
+ "id, session_id, workspace_id, seq, role, content, provider_uuid, parent_tool_use_id, created_at"
12353
+ ).abortSignal(AbortSignal.timeout(ROW_WRITE_TIMEOUT_MS)).single();
12354
+ if (error || !data) {
12355
+ if (refusedByPostgres(error?.code)) live.seq = seq - 1;
12356
+ throw new Error(`PROVIDER_ERROR: persist failed: ${error?.message ?? "no row"}`);
12357
+ }
12358
+ const row = {
12359
+ ...data,
12360
+ seq,
12361
+ role,
12362
+ content: safeContent
12363
+ };
12364
+ await this.broadcast(sessionId, { type: "message", message: row });
12365
+ return row;
12366
+ });
12367
+ live.writes = step.then(
12368
+ () => void 0,
12369
+ () => void 0
12370
+ );
12371
+ return step;
12246
12372
  }
12247
12373
  /**
12248
12374
  * The highest `chat_messages.seq` this session has, or 0. A failed read is fatal rather than
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jpcasabiancai/cayenne",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "files": [
5
5
  "dist/cli.js"
6
6
  ],