cd /news/ai-agents/kenapa-osfoundry-membuat-codex-cli-m… · home › topics › ai-agents › article
[ARTICLE · art-168892] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Kenapa osFoundry membuat Codex CLI melakukan polling cursor per resource alih-alih mengirim perubahan lewat push?

A developer's hands-on test of the osFoundry MCP server found that all three push paths are closed: initialize declares only tools with listChanged false, resources/subscribe returns "Method not found," and a GET to the MCP URL returns HTTP 405. The only remaining option is a read-only poll_resource_events tool that resumes from an event sequence per granted resource, which the developer argues suits non-daemon clients like Codex CLI that store a cursor integer and ask what changed after it.

by read8 min views1 publishedNov 13, 2026

Inilah semua yang ditawarkan server MCP osFoundry kepada client yang ingin tahu kapan sesuatu berubah. Dijalankan dari Mac ini tanpa token:

$ uv run --with tiktoken --with jsonschema python3 watch_sim.py
== Part 1: live wire, 8 calls each, ms ==
initialize                   HTTP 200  median  414.7  p90  757.8
tools/list                   HTTP 200  median  606.8  p90  716.7
resources/subscribe          HTTP 200  median  450.6  p90  550.2
GET (stream)                 HTTP 405  median  423.9  p90  475.4
tools/call poll (no token)   HTTP 401  median  322.0  p90  463.9
initialize capabilities: {"tools": {"listChanged": false}}
resources/subscribe -> {"code": -32601, "message": "Method not found"}
GET body -> ''
poll without token -> isError True | Authentication required
rate limit headers: limit 2000, reset in 61 s

Ketiga jalur push tertutup. initialize hanya mendeklarasikan tools, tidak ada yang lain, dengan listChanged: false, jadi daftar tools pun tidak akan mengumumkan perubahan. resources/subscribe mengembalikan "Method not found". GET ke URL MCP, tempat server Streamable HTTP biasanya membuka stream dari server ke client, mengembalikan 405. Yang tersisa hanya satu tool read-only:

{"name": "poll_resource_events",
 "description": "Resume from an event sequence for one granted channel, Note, knowledge base, record, or session.",
 "inputSchema": {"properties": {
     "resource_type": {"enum": ["message_channel", "note", "knowledge_base", "record_channel", "room",
                                "gitnest_repository", "agent_session", "team_session", "loop_session",
                                "graph_session"]},
     "resource_id": {"type": "string"},
     "after_sequence": {"type": "integer", "minimum": 0},
     "limit": {"type": "integer", "minimum": 1, "maximum": 100}},
   "required": ["resource_type", "resource_id"], "additionalProperties": false},
 "annotations": {"readOnlyHint": true, "openWorldHint": false, "destructiveHint": false},
 "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:read"]}]}

Resource "one granted", "resume from an event sequence". Sisa tulisan ini membahas kata-kata itu, dengan sisi client yang dibangun ulang di Python. Di mesin ini tidak ada binary codex, jadi script-nya yang berperan sebagai client. Kode server osFoundry tidak publik: Part 1 dan 2 adalah server live, sedangkan Part 3 adalah rekonstruksi dengan bentuk respons dan tingkat aktivitasnya sendiri.

Codex CLI bukan daemon. Ia jalan saat Anda menjalankannya, mengerjakan satu turn, lalu keluar atau menunggu Anda. Channel push butuh sesuatu yang terus mendengarkan di antara sesi Anda, dan di laptop sering kali tidak ada apa-apa. Cursor sequence cocok dengan pola itu: client menyimpan satu integer per resource, kembali kapan saja, dan menanyakan apa yang terjadi setelah angka itu.

Angka milidetik di atas diukur dari Jepang di Apple M1, jadi lebih banyak menggambarkan rute jaringan daripada server. Header rate limit lebih penting. Respons tanpa autentikasi membawa X-RateLimit-Limit: 2000 dengan reset sekitar 61 s ke depan. Belum dicek apakah client yang sudah login mendapat jatah yang sama, tetapi polling 30 resource setiap 30 s pun hanya 60 request per menit. Yang membatasi watcher berbasis Codex bukan beban server, melainkan turn-nya sendiri: setiap poll adalah tool call yang harus dibuat dan dibaca oleh model.

Dengan dua call, dan schema-nya banyak bercerita tentang desainnya:

list_granted_resources   osfoundry:read  ro=True required=[] props=[]
list_project_resources   osfoundry:read  ro=True required=['project_id', 'resource_type'] props=['project_id', 'resource_type', 'search', 'offset', 'limit']
poll_resource_events     osfoundry:read  ro=True required=['resource_type', 'resource_id'] props=['resource_type', 'resource_id', 'after_sequence', 'limit']
59 tools on server; these three = 591 of 12662 tokens (4.7%, cl100k_base)
poll_resource_events types: 10 | list_project_resources types: 9 | only pollable: ['graph_session']

list_granted_resources sama sekali tidak menerima argumen, dan schema-nya menutup kemungkinan menambahkannya:

list_granted_resources   no arguments           ok
list_granted_resources   project_id passed      REJECT additionalProperties

Di sini server yang memberi tahu client apa yang dimilikinya, bukan client yang meminta sesuatu. Instruksi initialize diawali dengan "Use list_granted_resources first." Jawabannya berisi project yang ditambahkan ke koneksi dan resource yang diberikan secara individual. Di web client (ExternalAssistantsPanel.tsx), keduanya adalah bagian Projects dan "Older individual access".

list_project_resources membutuhkan resource_type. Tidak ada listing "semua isi project ini", jadi inventaris lengkap berarti satu call per tipe, sembilan tipe, dan lebih banyak lagi kalau satu tipe punya lebih dari 50 item (limit maksimum 50, paging dengan offset). Deskripsinya menambahkan satu-satunya pengecualian untuk akses project: "Ordinary Message channels require a separate invitation."

graph_session bisa di-poll tetapi tidak bisa di-list. Graph tinggal di desktop owner dan masuk lewat list_graph_sessions, itu pun hanya setelah owner membagikannya dari aplikasi desktop.

osFoundry punya keduanya. wait_for_messages di jaringan agent menyimpan cursor untuk Anda: "Without after_sequence it resumes where your last call stopped", dan ia melakukan long polling dengan timeout_s sampai 50. poll_resource_events tidak punya keduanya. timeout_s ditolak, dan deskripsinya hanya bilang "resume from", jadi client yang membawa angkanya:

wait_for_messages props: ['after_sequence', 'timeout_s', 'limit'] timeout_s max 50 | poll_resource_events has timeout: False
poll_resource_events     timeout_s 30           REJECT additionalProperties

Cursor di sisi server nyaman untuk satu listener. Ia rusak saat dua client berbagi satu koneksi, dan itulah yang terjadi saat Anda membuka Codex di dua terminal dengan login yang sama. Rekonstruksi ini mengalirkan 600 event channel ke dua terminal yang bergantian melakukan poll secara acak:

two terminals, client cursor: A saw 600/600, B saw 600/600
two terminals, server cursor: A saw 305/600, B saw 295/600

Dengan satu cursor per koneksi di server, setiap terminal memajukan cursor milik terminal lain, dan masing-masing hanya melihat sekitar separuh. Dengan after_sequence di request, keduanya melihat semuanya, dan server sama sekali tidak menyimpan state per pembaca. Untuk log resource yang bisa dibaca beberapa tool secara terpisah, client adalah pemilik cursor yang tepat.

Schema tidak mewajibkan after_sequence, dan juga tidak menjelaskan apa artinya kalau dihilangkan. Jangan mengandalkannya. Simpan angkanya dan selalu kirim.

limit maksimal 100. Setelah bulk import, sebuah note bisa punya ratusan event yang menunggu. Untuk menghabiskannya, panggil lagi dari sequence terakhir sampai halaman yang kembali tidak penuh:

burst of 350 events, limit 100:  4 calls to drain, cursor ends at 350
burst of 350 events, limit  50:  7 calls to drain, cursor ends at 350
burst of 350 events, limit  20: 18 calls to drain, cursor ends at 350

Minta 100. Di rekonstruksi, ukuran respons adalah 17 token untuk halaman kosong dan 41 untuk satu event, dihitung dengan cl100k_base. Itu bentuk respons rekonstruksi, bukan milik osFoundry, tetapi orde besarnya sudah cukup menjelaskan: poll kosong murah untuk dibaca, jadi biaya polling sebagian besar ada di jumlah turn.

Rekonstruksi ini memberi satu project 30 resource dari sembilan tipe yang bisa di-list, dengan aktivitas yang timpang: 3 sibuk (satu event per menit), 7 sedang (satu setiap 10 menit), 20 sepi (satu setiap 2 jam). Angka-angka ini asumsi. Simulasinya menjalankan hari kerja 8 jam dengan lima seed, di jam simulasi:

every 30 s, all 30                 calls/hour   3600  empty  94.7%  seen late median   14.9 s  p90   27.0 s  max     30 s  (1843 events/day)
every 5 min, all 30                calls/hour    360  empty  78.0%  seen late median  150.9 s  p90  271.1 s  max    300 s  (1843 events/day)
30 s, x2 when empty, cap 10 min    calls/hour    502  empty  70.2%  seen late median   34.9 s  p90  206.4 s  max    599 s  (1843 events/day)
30 s, x2 when empty, cap 30 min    calls/hour    420  empty  65.3%  seen late median   35.6 s  p90  238.4 s  max   1799 s  (1843 events/day)
server window for comparison: 2000 requests per ~61 s

Tetap setiap 30 s itu cepat tetapi 95% terbuang: 3,600 call per jam, hampir semuanya kosong. Tetap setiap 5 menit itu murah tetapi lambat di mana-mana, termasuk di channel yang sibuk. Backoff per resource, yang menggandakan interval sebuah resource setelah setiap poll kosong dan me-reset-nya saat event pertama datang, mendekati jumlah call jadwal murah dengan median keterlambatan mendekati jadwal cepat. Penyebabnya, resource sibuk tetap di 30 s sementara resource sepi makin jarang di-poll. Harganya ada di batas atas: dengan 30 menit, perubahan pertama di resource sepi bisa menunggu setengah jam.

Untuk Codex, tabel ini sebenarnya lebih menjadi alasan untuk tidak memakai timer sama sekali. Saat seseorang bertanya "apa yang berubah?", satu putaran dengan cursor tersimpan hanya butuh satu call per resource yang dipantau, dan menjawab tanpa keterlambatan untuk semua yang terjadi sampai saat itu. Jadwal backoff ditujukan untuk script yang membuat Codex, atau apa pun, terus memantau.

Owner menghapus project di 4 h 07 min. Ke-30 cursor sekarang menunjuk ke resource yang tidak bisa dibaca oleh koneksi. Pertanyaannya, berapa lama client terus melakukan polling ke sana:

owner removes the project at 4 h 07 min (30 resources lose access):
  re-list grants every 10 min    failed polls after removal  180, list_granted_resources calls 25
  re-list grants every 60 min    failed polls after removal 3180, list_granted_resources calls 5
  re-list grants on first error  failed polls after removal    1, list_granted_resources calls 5

Mengecek grant dengan timer berarti antara menghabiskan call atau membuang ribuan poll gagal. Memperlakukan poll pertama yang ditolak sebagai sinyal "baca ulang grant" hanya memakan satu kegagalan dan satu list_granted_resources tambahan. Setelah itu, buang setiap cursor yang resource-nya sudah tidak ada di daftar. Kalau owner menambahkan project itu lagi nanti, client memulai resource tersebut dari awal.

Kasus sisanya, dicek dengan jsonschema (Draft 2020-12) terhadap inputSchema live:

list_project_resources   no resource_type       REJECT required
list_project_resources   limit 50               ok
list_project_resources   limit 51               REJECT maximum: limit
list_project_resources   graph_session          REJECT enum: resource_type
list_project_resources   search 101 chars       REJECT maxLength: search
poll_resource_events     no after_sequence      ok
poll_resource_events     graph_session          ok
poll_resource_events     after_sequence -1      REJECT minimum: after_sequence
poll_resource_events     limit 100              ok
poll_resource_events     limit 101              REJECT maximum: limit
poll_resource_events     resource_type project  REJECT enum: resource_type

resource_type: "project" adalah yang paling sering dicoba pertama kali. Project bukan resource yang punya log. Project adalah grant yang membuat resource terlihat.

Seluruh script memakan 20.2 s waktu nyata di M1, termasuk 40 network call. Waktu CPU user-nya 0.91 s.

Cara osFoundry menentukan tools yang didapat setiap koneksi ada di halaman tool scope. Definisi singkat protokolnya ada di entri glosarium MCP.

── more in #ai-agents 4 stories · sorted by recency
── more on @osfoundry 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/kenapa-osfoundry-mem…] indexed:0 read:8min 2026-11-13 · —