@cocorograph/hub-agent 0.8.37 → 0.8.38

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.37",
3
+ "version": "0.8.38",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -60,7 +60,7 @@
60
60
  */
61
61
 
62
62
  import { watch } from "node:fs"
63
- import { chmod, lstat, mkdir, readFile, writeFile, rename, unlink, readdir } from "node:fs/promises"
63
+ import { chmod, lstat, mkdir, readFile, writeFile, rename, stat, unlink, readdir } from "node:fs/promises"
64
64
  import { execFile as execFileCallback } from "node:child_process"
65
65
  import { promisify } from "node:util"
66
66
  import path from "node:path"
@@ -73,9 +73,22 @@ export const SIBLING_REQUESTS_DIR =
73
73
 
74
74
  export const ACTIVE_DELEGATIONS_PER_SESSION_LIMIT = 100
75
75
  export const DELEGATION_LEDGER_RETENTION_MS = 24 * 60 * 60 * 1000
76
+ const SIBLING_REQUEST_MAX_BYTES = 1024 * 1024
77
+ const DELEGATE_PROMPT_MAX_LENGTH = 64_000
76
78
 
77
79
  const execFile = promisify(execFileCallback)
78
80
 
81
+ function truncateDelegatePrompt(prompt) {
82
+ if (prompt.length <= DELEGATE_PROMPT_MAX_LENGTH) return prompt
83
+ let end = DELEGATE_PROMPT_MAX_LENGTH
84
+ const lastCodeUnit = prompt.charCodeAt(end - 1)
85
+ if (lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff) end -= 1
86
+ return (
87
+ prompt.slice(0, end) +
88
+ `\n\n[依頼本文はここで打ち切られています(全 ${prompt.length} 文字)]`
89
+ )
90
+ }
91
+
79
92
  /**
80
93
  * `ps` の起動時刻を送受信双方で比較する契約形へ正規化する。
81
94
  * 先頭・末尾の Unicode 空白だけを除去し、日時本体の内部空白は変更しない。
@@ -506,6 +519,30 @@ export class SiblingInjectBridge {
506
519
  const fp = path.join(this.dir, filename)
507
520
  let body
508
521
  try {
522
+ const fileStat = await stat(fp)
523
+ if (fileStat.size > SIBLING_REQUEST_MAX_BYTES) {
524
+ this.logger?.warn(
525
+ { filename, size: fileStat.size, max_size: SIBLING_REQUEST_MAX_BYTES },
526
+ "sibling-inject: request file rejected because it exceeds the size limit",
527
+ )
528
+ // ⚠️ **結果ファイルを必ず書く**。ここで書かずに戻ると、結果をポーリングしている
529
+ // 呼び出し側 (cockpit_orch.py) には「タイムアウト」としか見えない。タイムアウトは
530
+ // 「不達とは限らない」と扱う契約なので、拒否した事実が呼び出し側で不達に化ける
531
+ // (=黙って捨てたのと同じになる)。delegate-event の送り手は結果を読まないため、
532
+ // op を判別できないまま無条件に書いて構わない (余分な結果ファイルは stale 掃除の対象)。
533
+ await this._writeResult(request_id, {
534
+ ok: false,
535
+ error: "request_too_large",
536
+ size: fileStat.size,
537
+ max_size: SIBLING_REQUEST_MAX_BYTES,
538
+ })
539
+ try {
540
+ await unlink(fp)
541
+ } catch {
542
+ /* 既に無ければ no-op */
543
+ }
544
+ return
545
+ }
509
546
  body = JSON.parse(await readFile(fp, "utf-8"))
510
547
  } catch {
511
548
  // partial write / 既に消えた → 次の watch イベントで拾い直す (seen から外す)。
@@ -534,6 +571,7 @@ export class SiblingInjectBridge {
534
571
  phase,
535
572
  agent,
536
573
  description,
574
+ prompt,
537
575
  timestamp,
538
576
  pid,
539
577
  pid_started_at,
@@ -556,6 +594,7 @@ export class SiblingInjectBridge {
556
594
  (phase === "started" && (typeof agent !== "string" || !agent)) ||
557
595
  (agent !== undefined && typeof agent !== "string") ||
558
596
  (description !== undefined && typeof description !== "string") ||
597
+ (prompt !== undefined && typeof prompt !== "string") ||
559
598
  (pid !== undefined && (!Number.isInteger(pid) || pid <= 0)) ||
560
599
  (pid_started_at !== undefined && typeof pid_started_at !== "string") ||
561
600
  (timestamp !== undefined &&
@@ -617,6 +656,7 @@ export class SiblingInjectBridge {
617
656
  phase,
618
657
  ...(agent !== undefined ? { agent } : {}),
619
658
  ...(description !== undefined ? { description } : {}),
659
+ ...(phase === "started" && prompt ? { prompt: truncateDelegatePrompt(prompt) } : {}),
620
660
  ...(pid !== undefined ? { pid } : {}),
621
661
  ...(pid_started_at !== undefined ? { pid_started_at } : {}),
622
662
  timestamp: timestamp || new Date().toISOString(),
@@ -163,7 +163,23 @@ function redactBlocks(blocks) {
163
163
  * @returns {Record<string, unknown>}
164
164
  */
165
165
  export function maskMessageObject(obj, toolNames) {
166
+ if (!MASK_ENABLED) return obj
166
167
  if (!obj || typeof obj !== "object") return obj
168
+
169
+ if (obj.type === "system" && obj.subtype === "codex_delegate") {
170
+ let changed = false
171
+ const next = { ...obj }
172
+ for (const field of ["prompt", "description"]) {
173
+ if (typeof obj[field] !== "string") continue
174
+ const masked = redactSecrets(obj[field])
175
+ if (masked !== obj[field]) {
176
+ next[field] = masked
177
+ changed = true
178
+ }
179
+ }
180
+ return changed ? next : obj
181
+ }
182
+
167
183
  const message = obj.message
168
184
  if (!message || typeof message !== "object") return obj
169
185
  const content = message.content