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

> Source: <https://dev.to/bbt-id/kenapa-osfoundry-membuat-codex-cli-melakukan-polling-cursor-per-resource-alih-alih-mengirim-4pb8>
> Published: 2026-11-13 08:40:29+00:00

Inilah semua yang ditawarkan server MCP [osFoundry](https://osfoundry.io/id) kepada client yang ingin tahu kapan sesuatu berubah. Dijalankan dari Mac ini tanpa token:

``` bash
$ 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](https://osfoundry.io/id/platform/tool-scope). Definisi singkat protokolnya ada di [entri glosarium MCP](https://osfoundry.io/id/glossary/mcp).
