Skip to content

feat(provider): add versioned external provider protocol - #1260

Open
yansigit wants to merge 2 commits into
1jehuang:masterfrom
yansigit:feat/provider-extension-protocol
Open

yansigit wants to merge 2 commits into
1jehuang:masterfrom
yansigit:feat/provider-extension-protocol

Conversation

@yansigit

Copy link
Copy Markdown

Summary

Add a versioned subprocess provider protocol so user-maintained external providers can survive jcode application upgrades without being embedded into the main binary.

Changes

  • Define versioned provider wire frames and compatibility negotiation.
  • Add a subprocess adapter with bounded I/O and lifecycle handling.
  • Add a fixture provider and protocol conformance coverage.
  • Document the external provider contract and packaging boundary.

The follow-up registry and GitHub /plugin lifecycle work is intentionally separate from this protocol foundation.

Validation

  • Rebasing onto current upstream/master completed successfully.
  • jcode-provider-protocol tests passed.
  • jcode-provider-subprocess tests passed.
  • Workspace metadata and focused formatting checks passed.

Closes #745

@greptile-apps

greptile-apps Bot commented Sep 15, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 2/5

Not safe to merge: valid provider operations can become permanently desynchronized, fail under concurrency, or remain blocked when a provider stops consuming input.

Findings

  1. P1 Timed-out Frames Desynchronize Stream
  2. P1 Concurrent Requests Consume Frames
  3. P1 Writes Bypass Request Deadline
Fix with agent prompt
### Issue 1
crates/jcode-provider-subprocess/src/lib.rs:150-155
If a handshake or request times out after only part of a JSONL frame arrives, the timeout drops the accumulated prefix while leaving stdout positioned after it. A later operation then reads only the remaining suffix and fails protocol decoding, making the provider unusable after a recoverable timeout. Preserve partial-frame state across calls or invalidate and restart the transport when a partial read times out.

### Issue 2
crates/jcode-provider-subprocess/src/lib.rs:238-259
Two `request_and_collect` calls independently consume the same stdout stream. When provider frames are interleaved, each collector can remove the other request's event and return `UnexpectedFrame`, so both otherwise valid requests fail. Serialize complete exchanges or route frames through one reader to per-request queues.

### Issue 3
crates/jcode-provider-subprocess/src/lib.rs:228-230
`request_and_collect` starts its deadline only after sending the request. If a provider stops reading stdin, a sufficiently large request fills the pipe and `write_all` blocks indefinitely, so the configured request timeout never fires and the caller remains stuck. Apply the same deadline to writing and flushing the request.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

  • A timed-out partial frame leaves the subprocess output stream unusable for later operations.
  • Concurrent requests can consume each other's correlated frames and fail valid requests.
  • An unresponsive provider can indefinitely block a large request while it is being written.

These failures affect the subprocess provider transport and must be resolved before merging.

Reviews (1) · Last reviewed commit: "feat(provider): add upgrade-safe externa..."

Comment on lines +150 to +155
let read = self.next_inner();
match timeout {
Some(timeout) => tokio::time::timeout(timeout, read)
.await
.map_err(|_| AdapterError::Timeout)?,
None => read.await,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Timed-out Frames Desynchronize Stream

If a handshake or request times out after only part of a JSONL frame arrives, the timeout drops the accumulated prefix while leaving stdout positioned after it. A later operation then reads only the remaining suffix and fails protocol decoding, making the provider unusable after a recoverable timeout. Preserve partial-frame state across calls or invalidate and restart the transport when a partial read times out.

Artifacts

Evidence from the check

  • The uploaded source contains the focused Tokio test and real Python subprocess fixture that writes one JSONL frame in two parts, ending with the asserted desynchronization.

Command output from the check

  • The captured parent-source inspection shows the earlier transport used a single `read_until` call and had no `next_with_timeout` path, establishing the relevant before implementation.

Command output from the check

  • The executed focused test records a timeout after the emitted prefix and a malformed-protocol error when the subsequent call reads the remaining suffix, confirming the defect.

Command output from the check

  • The executed format check and complete subprocess library test suite passed all four tests, including the focused regression, confirming the harness compiles and runs with the changed transport.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/jcode-provider-subprocess/src/lib.rs
Line: 150-155

Comment:
**Timed-out Frames Desynchronize Stream**

If a handshake or request times out after only part of a JSONL frame arrives, the timeout drops the accumulated prefix while leaving stdout positioned after it. A later operation then reads only the remaining suffix and fails protocol decoding, making the provider unusable after a recoverable timeout. Preserve partial-frame state across calls or invalidate and restart the transport when a partial read times out.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +238 to +259
match &frame {
Frame::Event { request_id, .. } if request_id == &id => frames.push(frame),
Frame::Response {
id: response_id, ..
} if response_id == &id => {
frames.push(frame);
return Ok(frames);
}
Frame::HelloOk { .. } => {
return Err(AdapterError::UnexpectedFrame(
"received hello_ok after handshake".into(),
));
}
Frame::Event { request_id, .. } => {
return Err(AdapterError::UnexpectedFrame(format!(
"event for another request while waiting for {id}: {request_id}"
)));
}
other => {
return Err(AdapterError::UnexpectedFrame(format!(
"unexpected frame while waiting for {id}: {other:?}"
)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Concurrent Requests Consume Frames

Two request_and_collect calls independently consume the same stdout stream. When provider frames are interleaved, each collector can remove the other request's event and return UnexpectedFrame, so both otherwise valid requests fail. Serialize complete exchanges or route frames through one reader to per-request queues.

Knowledge Base Used: Provider selection and runtime adapters

Artifacts

Evidence from the check

  • The authored Rust integration test starts a Python JSONL fixture, validates a single-request control, and reproduces interleaved two-request frame handling; the test encodes the confirmed failure.

Command output from the check

  • Ran the single-request control from `/home/user/repo`; it received its matching event and response successfully, establishing normal non-interleaved behavior.

Command output from the check

  • Ran the two-request correlated-frame repro from `/home/user/repo`; request A consumed B's event and request B consumed A's event, confirming the defect.

Command output from the check

  • Ran both focused integration tests together from `/home/user/repo`; the control succeeded and the asserted concurrent failure reproduced, confirming the finding.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/jcode-provider-subprocess/src/lib.rs
Line: 238-259

Comment:
**Concurrent Requests Consume Frames**

Two `request_and_collect` calls independently consume the same stdout stream. When provider frames are interleaved, each collector can remove the other request's event and return `UnexpectedFrame`, so both otherwise valid requests fail. Serialize complete exchanges or route frames through one reader to per-request queues.

**Knowledge Base Used:** [Provider selection and runtime adapters](https://app.greptile.com/solo-systems/-/custom-context/knowledge-base/1jehuang/jcode/-/docs/provider-selection-and-runtime-adapters.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +228 to +230
let id = id.into();
self.request(id.clone(), method, params).await?;
let deadline = tokio::time::Instant::now() + self.config.request_timeout;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Writes Bypass Request Deadline

request_and_collect starts its deadline only after sending the request. If a provider stops reading stdin, a sufficiently large request fills the pipe and write_all blocks indefinitely, so the configured request timeout never fires and the caller remains stuck. Apply the same deadline to writing and flushing the request.

Artifacts

Evidence from the check

  • This authored executable integration test starts a Python provider that reads only the hello frame, then compares a small request with an 8 MiB request under a 50 ms configured timeout, demonstrating that the write is outside the configured deadline.

Command output from the check

  • The executed focused control test returned `Err(Timeout)` after 51 ms, showing the configured deadline applies once the small request write completes.

Command output from the check

  • The executed oversized-request test returned only the external watchdog's `Err(Elapsed(()))` after 510 ms despite a 50 ms configured request timeout, confirming the blocked write is not timed.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/jcode-provider-subprocess/src/lib.rs
Line: 228-230

Comment:
**Writes Bypass Request Deadline**

`request_and_collect` starts its deadline only after sending the request. If a provider stops reading stdin, a sufficiently large request fills the pipe and `write_all` blocks indefinitely, so the configured request timeout never fires and the caller remains stuck. Apply the same deadline to writing and flushing the request.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: package/extension system for sharing community-developed features (like Pi packages)

1 participant