@gemus/mcp-proxy 0.1.4 → 0.1.5

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": "@gemus/mcp-proxy",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Local stdio<->HTTP MCP proxy for Codex desktop: transparent passthrough + execute tap + imagegen backfill (Issue #1751, P1).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@modelcontextprotocol/sdk": "^1.29.0",
28
- "@gemus/codex-backfill-core": "0.1.2"
28
+ "@gemus/codex-backfill-core": "0.1.3"
29
29
  },
30
30
  "engines": {
31
31
  "node": ">=18"
@@ -74,7 +74,7 @@ beforeEach(() => {
74
74
  mocks.readRollout.mockReset().mockReturnValue('')
75
75
  mocks.findRolloutFile.mockReset().mockReturnValue('/fake/rollout.jsonl')
76
76
  // Default: generated_images unavailable → backfill falls back to the rollout source (existing tests).
77
- mocks.readGeneratedImageFiles.mockReset().mockReturnValue({ available: false, images: [] })
77
+ mocks.readGeneratedImageFiles.mockReset().mockReturnValue({ available: false, images: [], failedCallIds: [] })
78
78
  })
79
79
 
80
80
  describe('normalizeToolName (tap → tracker)', () => {
@@ -353,4 +353,42 @@ describe('backfillTurn — Layer 2 file byte source (#1756)', () => {
353
353
  expect(mocks.readRollout).toHaveBeenCalledWith('/x')
354
354
  expect(r.delivered).toBe(1)
355
355
  })
356
+
357
+ it('exec-* file-source image (opaque call-id) is delivered exactly once (#1968)', async () => {
358
+ const bf = makeBackfiller()
359
+ observeExecuteAnchor(bf, 1, 't1', 's1', 'wf_real', 'nodeA')
360
+ mocks.readGeneratedImageFiles.mockReturnValue({
361
+ available: true,
362
+ images: [{ callId: 'exec-3eb2a29c', base64: 'iVBORw0KGgoAAAA1', mimeType: 'image/png' }],
363
+ failedCallIds: [],
364
+ })
365
+
366
+ const r = await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/x', salvageOnly: false })
367
+
368
+ expect(mocks.readRollout).not.toHaveBeenCalled()
369
+ expect(r.delivered).toBe(1)
370
+ expect(mocks.deliverImageToNode).toHaveBeenCalledTimes(1)
371
+ expect(mocks.deliverImageToNode.mock.calls[0][0]).toMatchObject({
372
+ nodeId: 'nodeA',
373
+ image: { callId: 'exec-3eb2a29c' },
374
+ })
375
+ })
376
+
377
+ it('unreadable fresh candidate is recovered from rollout by EXACT call-id only — no history bypass (#1968)', async () => {
378
+ const bf = makeBackfiller()
379
+ observeExecuteAnchor(bf, 1, 't1', 's1', 'wf_real', 'nodeA')
380
+ // File source: nothing readable, but one fresh candidate failed to read (torn write).
381
+ mocks.readGeneratedImageFiles.mockReturnValue({ available: true, images: [], failedCallIds: ['exec-torn'] })
382
+ // Rollout holds the failed id AND an unrelated historical image that must NOT be surfaced.
383
+ mocks.readRollout.mockReturnValue(rollout([['exec-torn', 'iVBORw0KGgoAAAA9'], ['ig_history', 'iVBORw0KGgoAAAA8']]))
384
+
385
+ const r = await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/x', salvageOnly: false })
386
+
387
+ expect(mocks.readRollout).toHaveBeenCalledWith('/x')
388
+ expect(r.delivered).toBe(1)
389
+ expect(mocks.deliverImageToNode).toHaveBeenCalledTimes(1)
390
+ expect(mocks.deliverImageToNode.mock.calls[0][0]).toMatchObject({ image: { callId: 'exec-torn' } })
391
+ // The historical image was in the rollout but is NOT in failedCallIds → never delivered or orphaned.
392
+ expect(mocks.addImageToCanvas).not.toHaveBeenCalled()
393
+ })
356
394
  })
package/src/backfill.mjs CHANGED
@@ -137,17 +137,33 @@ export function createBackfiller({ server, apiKey, codexHome, call, log = () =>
137
137
  async function backfillTurnInner({ turnKey, sessionId, rolloutPath, salvageOnly }) {
138
138
  const t = turns.get(turnKey)
139
139
  const sid = sessionId || t?.sessionId
140
- // #1756 Layer 2: 优先读 generated_images 落盘文件(去 rollout-scrape 脆化)。三态 fallback——
141
- // 仅目录整体不可用(旧 codex 不落盘)才回退 rollout;目录可用即锁定文件源(即便过滤后空),
142
- // 否则会绕过 mtime 守卫把历史图从 rollout 重新捞出重复交付。
140
+ // #1756 Layer 2: 优先读 generated_images 落盘文件(去 rollout-scrape 脆化)。仅目录整体不可用
141
+ // (旧 codex 不落盘)才整源回退 rollout;目录可用即锁定文件源,否则会绕过 mtime 守卫把历史图从
142
+ // rollout 重新捞出重复交付。#1968:文件源里「已确认 fresh 但读失败」的候选(torn/locked write)
143
+ // 按精确 call-id 定向从 rollout 恢复——只捞这几个 id,不换整源、不越 mtime 守卫。
143
144
  const fileResult = sid
144
145
  ? core.readGeneratedImageFiles(codexHome, sid, procStartMs)
145
- : { available: false, images: [] }
146
+ : { available: false, images: [], failedCallIds: [] }
146
147
  let all
147
148
  let source
148
149
  if (fileResult.available) {
149
150
  all = fileResult.images
150
151
  source = 'generated_images'
152
+ const failedCallIds = fileResult.failedCallIds || []
153
+ if (failedCallIds.length) {
154
+ const path = rolloutPath || core.findRolloutFile(codexHome, sid)
155
+ const want = new Set(failedCallIds)
156
+ const recovered = core.extractImageOutputs(core.readRollout(path)).filter((img) => want.has(img.callId))
157
+ if (recovered.length) {
158
+ all = all.concat(recovered)
159
+ source = `${source}+rollout`
160
+ }
161
+ log('backfill: unreadable generated_images candidates', {
162
+ turnKey,
163
+ failed: failedCallIds.length,
164
+ recovered: recovered.length,
165
+ })
166
+ }
151
167
  } else {
152
168
  const path = rolloutPath || core.findRolloutFile(codexHome, sid)
153
169
  all = core.extractImageOutputs(core.readRollout(path))