@cocorograph/hub-agent 0.8.71 → 0.8.72

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.8.71",
3
+ "version": "0.8.72",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -980,6 +980,8 @@ export class CodexStreamBridge extends EventEmitter {
980
980
  /** 取消成功 id は accepted(二重実行ガード)から外さず、browser が「accepted +キュー不在」
981
981
  * のバブル昇格から除外するための別軸として保持する。 */
982
982
  cancelledInputIds: [],
983
+ /** 取消が input より先着した場合の guard。後着 input で消費する。 */
984
+ canceledInputIds: new Set(),
983
985
  /** @type {{resolve: Function, timer: any} | null} round3 major#2: turn/start 後に turn/started
984
986
  * (activeTurnId 確定) を待つ barrier の待機者。timeout 付きでハングを防ぐ。 */
985
987
  _turnActiveWaiter: null,
@@ -2166,16 +2168,34 @@ export class CodexStreamBridge extends EventEmitter {
2166
2168
  /** 次ターン FIFO に残っている browser 入力を id 指定で取り消す。
2167
2169
  * dispatchingInput は既に turn/start RPC 中なので対象外(実行開始済みとして扱う)。 */
2168
2170
  cancelQueued({ stream_id, id }) {
2169
- const session = this._require(stream_id)
2170
- if (!id) return false
2171
+ const session = this.sessions.get(stream_id)
2172
+ if (!session || session.dead) {
2173
+ return { outcome: "stream_missing", flush: () => {} }
2174
+ }
2175
+ if (typeof id !== "string" || !id) throw new Error("invalid_input_id")
2171
2176
  const index = session.inputQueue.findIndex(
2172
2177
  (item) =>
2173
2178
  (item?.inputId === id || item?.deliveryId === id) &&
2174
2179
  item !== session.dispatchingInput,
2175
2180
  )
2176
2181
  if (index < 0) {
2177
- this._emitQueueState(session)
2178
- return false
2182
+ // accepted は二重実行防止の記録。取消済みなら再要求でも実行開始済みに変えない。
2183
+ if (session.cancelledInputIds?.includes(id)) {
2184
+ return { outcome: "cancelled", flush: () => this._emitQueueState(session) }
2185
+ }
2186
+ const dispatching = session.dispatchingInput
2187
+ const alreadyStarted =
2188
+ dispatching?.inputId === id ||
2189
+ dispatching?.deliveryId === id ||
2190
+ session.acceptedInputIds?.includes(id)
2191
+ if (!alreadyStarted) {
2192
+ if (session.canceledInputIds.size >= 1024) session.canceledInputIds.clear()
2193
+ session.canceledInputIds.add(id)
2194
+ }
2195
+ return {
2196
+ outcome: alreadyStarted ? "already_started" : "cancelled",
2197
+ flush: () => this._emitQueueState(session),
2198
+ }
2179
2199
  }
2180
2200
  const [item] = session.inputQueue.splice(index, 1)
2181
2201
  this._recordCancelledInput(session, item.inputId || item.deliveryId)
@@ -2185,8 +2205,8 @@ export class CodexStreamBridge extends EventEmitter {
2185
2205
  })
2186
2206
  item.reject(err)
2187
2207
  }
2188
- this._emitQueueState(session)
2189
- return true
2208
+ // main が outcome を送った後で snapshot を送る。
2209
+ return { outcome: "cancelled", flush: () => this._emitQueueState(session) }
2190
2210
  }
2191
2211
 
2192
2212
  _emitQueueState(session, startedItem = null) {
@@ -3573,6 +3593,12 @@ export class CodexStreamBridge extends EventEmitter {
3573
3593
  this._touch(session, stream_id)
3574
3594
  const input = userInputFromMessage(message)
3575
3595
  const text = input.map((part) => part?.text || "").join("\n")
3596
+ if (inputId && session.canceledInputIds.delete(inputId)) {
3597
+ this._recordAcceptedInput(session, inputId)
3598
+ this._recordCancelledInput(session, inputId)
3599
+ this._emitQueueState(session)
3600
+ return { method: "queued", deliveryId }
3601
+ }
3576
3602
  if (inputId && session.acceptedInputIds?.includes(inputId)) {
3577
3603
  return { method: session.activeTurnId ? "queued" : "turn/start", deliveryId }
3578
3604
  }
package/src/main.mjs CHANGED
@@ -2331,6 +2331,8 @@ export function buildCodexReadyPayload(streamId, info, queueState) {
2331
2331
  // Browser は旧 hub-agent との混在期間、この明示 capability が true のときだけ
2332
2332
  // 子 thread の個別停止 UI を有効化する。省略は未対応を意味する。
2333
2333
  supports_subagent_interrupt: true,
2334
+ // 旧 agent との混在時、取消結果を待てることを明示する。
2335
+ supports_queue_cancel_response: true,
2334
2336
  }
2335
2337
  if (Number.isInteger(info?.historyStart)) payload.history_start = info.historyStart
2336
2338
  if (Number.isInteger(info?.historyTotal)) payload.history_total = info.historyTotal
@@ -4797,9 +4799,16 @@ export async function dispatch(msg, ctx) {
4797
4799
  // turn/steer / turn/interrupt は明示操作の別経路として扱う。
4798
4800
  handleCodexChatInput(msg, ctx)
4799
4801
  return
4800
- case "codex.queue.cancel":
4802
+ case "codex.queue.cancel": {
4803
+ const cancelRes = {
4804
+ type: "codex.queue.response",
4805
+ stream_id: msg.stream_id,
4806
+ request_id: msg.request_id || null,
4807
+ input_id: msg.id || null,
4808
+ }
4809
+ let canceled
4801
4810
  try {
4802
- ctx.codexBridge?.cancelQueued({
4811
+ canceled = ctx.codexBridge?.cancelQueued({
4803
4812
  stream_id: msg.stream_id,
4804
4813
  id: msg.id,
4805
4814
  })
@@ -4808,8 +4817,13 @@ export async function dispatch(msg, ctx) {
4808
4817
  { err: err?.message, stream_id: msg.stream_id, input_id: msg.id },
4809
4818
  "codex queued input cancel failed",
4810
4819
  )
4820
+ ctx.client.send({ ...cancelRes, error: err?.message || "cancel_failed" })
4821
+ return
4811
4822
  }
4823
+ ctx.client.send({ ...cancelRes, outcome: canceled?.outcome || "stream_missing" })
4824
+ canceled?.flush?.()
4812
4825
  return
4826
+ }
4813
4827
  case "codex.permission.reply":
4814
4828
  if (!ctx.codexBridge) return
4815
4829
  ctx.codexBridge.permissionReply({