Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
d21c63d
docs(java): add ADR-007 native runtime bundling strategy
edburns Jul 6, 2026
ba4190b
Potential fix for pull request finding
edburns Jul 6, 2026
eb793f6
Update adr-007 to address decision to not use Panama
edburns Jul 10, 2026
f1b74e2
Select Option 2 and Option 1
edburns Jul 10, 2026
f4c5782
GUTDODP
edburns Jul 21, 2026
ee6e6f7
Add ignorance reduction plan for Java SDK native runtime embedding (#…
edburns Jul 22, 2026
6f9a716
GUTDODP
edburns Jul 22, 2026
a62433a
Get answers to pre-traversal questions in section 3.15.
edburns Jul 22, 2026
1b107ba
GUTDODP
edburns Jul 22, 2026
276d80a
Phase 3, 3.1 WIP
edburns Jul 22, 2026
61882a1
Rethink answer for 3.1.
edburns Jul 22, 2026
88046b6
Working through 3.1 and 3.2
edburns Jul 22, 2026
0366305
Resolve 3.1 and 3.2: hybrid reactor structure + npm pack binary acqui…
edburns Jul 22, 2026
23ffad8
GUTDODP
edburns Jul 22, 2026
c509abe
Expand step 4.6 into 4.6a/b/c for hybrid reactor restructure
edburns Jul 22, 2026
0c312bc
3.3 resolution: interface + direct instantiation + com.github.copilot…
edburns Jul 22, 2026
ed993c9
Spike 3.4: JNA callback threading with Rust DLL
edburns Jul 22, 2026
50588d0
3.4 resolution: QueueInputStream + MR-JAR virtual thread reader
edburns Jul 22, 2026
67dfe1d
GUTDODP
edburns Jul 22, 2026
0864dbd
3.5.1 resolution: RuntimeConnection sealed class hierarchy
edburns Jul 22, 2026
ddf0d7b
3.5.2 resolution: FfiRuntimeHost parallel to CliServerManager
edburns Jul 22, 2026
28bf648
3.5.3 resolution: JsonRpcClient.fromStreams() + QueueInputStream + Ff…
edburns Jul 22, 2026
90013db
GUTDODP
edburns Jul 22, 2026
91f5a30
GUTDODP
edburns Jul 22, 2026
9952c17
GUTDODP
edburns Jul 23, 2026
fc0b8eb
Spike 3.6 darwin-arm64
edburns Jul 23, 2026
d678fdc
Make spikes into maven projects
edburns Jul 23, 2026
b85e8c0
3.6 Windows x64
edburns Jul 23, 2026
61ae299
Document 3.6 platform detection resolution
edburns Jul 23, 2026
864df66
GUTDODP
edburns Jul 23, 2026
aa12a97
Resolve native runtime extraction caching strategy
edburns Jul 24, 2026
9137379
Document JNA GraalVM callback spike
edburns Jul 24, 2026
86adb3b
GUTDODP
edburns Jul 24, 2026
80845f1
Expand Question 3.9 to cover all five C ABI parameters
edburns Jul 24, 2026
3276235
Correct ABI table: argv_json example, host_start blocking, nullable p…
edburns Jul 24, 2026
2574c99
3.9: C ABI parameter semantics spike and resolution
edburns Jul 24, 2026
8fefc7b
3.10: Error handling and diagnostics resolution
edburns Jul 24, 2026
40c15a4
GUTDODP
edburns Jul 24, 2026
143fe54
spike-3-11: InProcess E2E transport on win32-x64 + 3.11 Resolution
edburns Jul 27, 2026
eb92600
GUTDODP
edburns Jul 27, 2026
114e4fa
GUTDODP
edburns Jul 27, 2026
fc72974
GUTDODP
edburns Jul 27, 2026
6079ada
3.12 Resolution + Phase 4 linux-x64-first DRI note
edburns Jul 27, 2026
189a805
Provide resolutions for 3.13 and 3.14
edburns Jul 28, 2026
fbb2e9f
GUTDODP
edburns Jul 28, 2026
46a9546
Fix 6 LLM-friendliness inconsistencies in Phase 4 implementation steps
edburns Jul 28, 2026
8a45af6
GUTDODP
edburns Jul 28, 2026
31364a1
docs(plan): normalize linux-x64-only invariant wording
edburns Jul 28, 2026
a29fa7f
docs(plan): add Phase 4 implementation sequence diagram
edburns Jul 28, 2026
6237ced
docs(plan): include all pending modified files
edburns Jul 28, 2026
f95aa87
docs(plan): fix Mermaid parse in Phase 4 sequence diagram
edburns Jul 28, 2026
a951d61
docs(plan): fix second Mermaid parse error in phase 4 diagram
edburns Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Phase 4 Sequence Diagram (Implementation Order)

```mermaid
sequenceDiagram
autonumber
actor DRI as Human DRI
participant Agent as Coding Agent
participant Tests as Unit/IT Tests
participant Build as Maven Build
participant Repo as Git Branch
participant CI as GitHub Actions CI

Note over DRI,CI: Phase 4 invariant: native implementation scope is Ubuntu 24.04 linux-x64 only.
Note over DRI,CI: Out of scope in this phase: linux-arm64, linuxmusl-x64, linuxmusl-arm64, darwin-x64, darwin-arm64, win32-x64, win32-arm64.

DRI->>Agent: Start Phase 4 implementation

rect rgba(240, 248, 255, 0.6)
Note over Agent,Build: TDD cycle applied to each implementation step
loop For each step (4.1, 4.2, 4.3, 4.4, 4.5, 4.6a, 4.6b, 4.7, 4.8)
Agent->>Tests: Write tests first (red)
Agent->>Agent: Implement minimum production code (green)
Agent->>Agent: Refactor and run spotless
Agent->>Build: Run mvn verify (current + all prior steps)
Build-->>Agent: Gate pass/fail
alt Gate fails
Agent->>Agent: Fix code/tests and re-run gate
else Gate passes
Agent->>Repo: Commit step
end
end
end

Note over Agent,Tests: Step 4.1 PlatformDetector. linux-x64 correctness gate in this phase.
Note over Agent,Tests: Step 4.2 NativeRuntimeLoader. linux-x64 extraction and cache now. Uber-jar multi-platform readiness is for a later phase.
Note over Agent,Tests: Step 4.3 NativeBinding, JnaNativeBinding, and OutboundCallback.
Note over Agent,Tests: Step 4.4 FfiRuntimeHost lifecycle, callback drain, and write-close safety.
Note over Agent,Tests: Step 4.5 RuntimeConnection integration in CopilotClient, env var resolution, and compatibility validation.

Agent->>Build: Execute Step 4.6a (reactor restructure)
Build-->>Agent: Parent/sdk module structure green

Agent->>Build: Execute Step 4.6b (copilot-native packaging)
Note over Agent,Build: Current phase packages linux-x64 classifier only. Verify integrity hash for the linux-x64 tarball.
Build-->>Agent: linux-x64 classifier JAR produced

alt Step 4.6c requested now
Agent-->>DRI: Deferred by Phase 4 invariant policy for this round
else Step 4.6c not required now
Note over Agent,DRI: Keep 4.6c deferred in this phase.
end

Agent->>Tests: Execute Step 4.7 InProcessTransportIT (real runtime.node)
Tests-->>Agent: In-process linux-x64 flow verified

Agent->>CI: Update Step 4.8 workflow
Note over Agent,CI: InProcess CI coverage in this phase: ubuntu-latest (linux-x64) only.
CI-->>Agent: CI green

Agent->>Repo: Final Phase 4 ordered changes committed
Agent->>Repo: Push branch updates
Repo-->>DRI: Ready for human ordering/flow review
```

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
# DAY: 2026-07-10

## ghcp-sdk-1965-spike-how-hard-is-option-1-and-option-2-both


### 15:42 Prompt

Consider this response regarding the Panama vs. JNA/FFM question:

> The JNA calling for Java, is this not a case for FFI via Panama (assuming a Java 25 LTS base line).

I am leaning to **not** go with Panama. But how can I respond to this commenter?

### 15:53 Response

#### Full response

Great question — Panama FFM was seriously considered, and it's likely the eventual destination. For this iteration we're staying with JNA. Reasoning:

1. **Baseline.** The SDK supports Java 17, and FFM isn't available there (it finalized in 22 via JEP 454). A JNA path would be needed regardless, so FFM would be an *additional* binding implementation, not a replacement.

2. **Consumer friction.** FFM's restricted native access means every consumer would need `--enable-native-access=...` (or the manifest attribute) on their launcher as JEP 472 enforcement tightens. JNA works with zero configuration today. For an SDK, that flag becomes every downstream app's problem and a recurring support issue. (JNA sits on the same enforcement trajectory eventually, since it uses JNI internally — so this buys us time, not immunity.)

3. **The performance win doesn't materialize here.** FFM's advantage over JNA is per-call marshalling overhead. Our C ABI surface is ~12 stable `extern "C"` entry points carrying JSON-RPC strings; serialization cost dominates, and call frequency is bounded by agent-interaction rates, not tight loops. The latency delta would be unmeasurable end-to-end.

4. **Upcall complexity.** The transport is bidirectional, so the runtime calls back into Java from native threads. JNA's `Callback` semantics are well-worn; FFM upcall stubs require explicit `Arena` lifetime management, where a stub outlived by a Rust-held function pointer is a JVM crash.

5. **GraalVM native-image.** JNA's reachability metadata story is mature; FFM support (especially upcalls) is still maturing and varies by GraalVM release. Some plausible SDK consumers (Quarkus/Micronaut CLI tools) compile to native images.

That said, we're structuring the binding layer behind a small internal interface so an FFM implementation can be added later (e.g., as a multi-release JAR) without touching the transport logic. When the baseline moves past 17 and the JEP 472 endgame arrives, a ~12-function stable ABI makes that migration cheap.

#### Compact response

Good instinct — FFM is probably where this lands eventually, but not in this iteration, for three reasons:

- We support Java 17, where FFM doesn't exist, so JNA is required anyway; FFM would be a second parallel implementation.
- FFM pushes `--enable-native-access` onto every consumer's launcher as JEP 472 tightens; JNA is zero-config for downstream users today.
- The C ABI surface is ~12 fixed entry points carrying JSON-RPC strings, so JSON serialization dominates and FFM's call-overhead advantage is unmeasurable here. Bidirectional callbacks also add FFM `Arena`/upcall-lifetime complexity that JNA's `Callback` handles for free.

We're abstracting the binding layer behind an internal interface, so swapping in FFM later (once the baseline and JEP 472 timeline make it the clear win) is a contained change rather than a rewrite. Happy to expand any of this into the ADR if useful.

#### Following

Two drafts above — a full five-point rationale if you want the reasoning on the record (useful if this thread effectively becomes ADR documentation), and a tighter three-point version if the comment thread just needs closure.

Both open by validating the suggestion rather than rebutting it, because the commenter isn't wrong — FFM *is* the right long-term answer, just not the right answer for a 17-baseline SDK with a JSON-dominated transport today. Framing it as "eventual destination, deliberately deferred" also preempts the follow-up question ("so when?") by pointing at the concrete triggers: baseline moving past 17 and JEP 472 enforcement arriving.

One thing worth deciding before you post: whether to commit publicly to the binding-layer abstraction. Both drafts mention it, and it's a cheap promise that makes the "not now" more credible — but if it's not actually in ADR-007 yet, you may want to add it there first so the comment points at something real.

### 16:04 Prompt

Read `copilot-sdk/java/docs/adr/adr-007-native-bundling-strategy.md`.

I want to explore the ability to select Option 2, but use the `maven-assembly-plugin` to create an uber jar that contains the Java artifact and a select set of the native jars:

```
com.github:copilot-sdk-java-runtime:VERSION:linux-x64
com.github:copilot-sdk-java-runtime:VERSION:linux-arm64
com.github:copilot-sdk-java-runtime:VERSION:linuxmusl-x64
com.github:copilot-sdk-java-runtime:VERSION:linuxmusl-arm64
com.github:copilot-sdk-java-runtime:VERSION:darwin-x64
com.github:copilot-sdk-java-runtime:VERSION:darwin-arm64
com.github:copilot-sdk-java-runtime:VERSION:win32-x64
com.github:copilot-sdk-java-runtime:VERSION:win32-arm64
```

If we have such an uber jar, how can the java artifact select the correct native jar content for the JNA interaction?

### 16:09 Response

Updated todo list

Read [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-007-native-bundling-strategy.md#1-1), lines 1 to 200

Read [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-007-native-bundling-strategy.md#200-200), lines 200 to 229

Now I have the full picture. Let me look at what native loading infrastructure exists in the Java SDK currently.

Searched for files matching `**/java/src/**/*Native*`, no matches

Searched for files matching `**/java/src/**/*Platform*`, 2 matches

Searched for files matching `**/java/src/**/*Runtime*`, 1 match

Searched for regex `JNA|jna|native.*load|NativeLoader` (`**/java/src/**`), 2 results

No native loading code exists yet in the Java SDK — this is greenfield. Here's how the uber-jar approach works with Option 2's per-platform classifier JARs:

---

## How it works: classpath resource convention + platform detection

### 1. Each classifier JAR uses a well-known resource path

Each per-platform JAR (`copilot-sdk-java-runtime:VERSION:darwin-arm64`, etc.) places its binary under a deterministic path inside the JAR:

```
native/darwin-arm64/runtime.node
native/darwin-arm64/platform.properties
```

When `maven-assembly-plugin` creates the uber-jar, it unpacks all dependencies and merges them. The resulting uber-jar contains:

```
com/github/copilot/sdk/... (Java classes)
native/linux-x64/runtime.node
native/linux-arm64/runtime.node
native/linuxmusl-x64/runtime.node
native/linuxmusl-arm64/runtime.node
native/darwin-x64/runtime.node
native/darwin-arm64/runtime.node
native/win32-x64/runtime.node
native/win32-arm64/runtime.node
```

### 2. The coordination artifact selects at runtime via `getResourceAsStream`

```java
public class NativeRuntimeLoader {

public Path loadRuntime() {
String classifier = detectPlatformClassifier();
String resourcePath = "native/" + classifier + "/runtime.node";

try (InputStream in = getClass().getClassLoader()
.getResourceAsStream(resourcePath)) {
if (in == null) {
throw new UnsupportedOperationException(
"No native runtime for platform: " + classifier);
}
Path cached = getCachePath(classifier);
if (!Files.exists(cached)) {
Files.createDirectories(cached.getParent());
Files.copy(in, cached);
// Make executable on Unix
cached.toFile().setExecutable(true);
}
return cached;
}
}

private String detectPlatformClassifier() {
String os = normalizeOs(System.getProperty("os.name"));
String arch = normalizeArch(System.getProperty("os.arch"));
String libc = "linux".equals(os) ? detectLinuxLibc() : "";

// Produces: "linux-x64", "linuxmusl-arm64", "darwin-arm64", "win32-x64", etc.
return (libc.isEmpty() ? os : os + libc) + "-" + arch;
}

private String detectLinuxLibc() {
// Read ELF PT_INTERP from /proc/self/exe
// If interpreter contains "/ld-musl-" → "musl"
// Otherwise → "" (glibc is the default/unmarked case for "linux-")
// ...
}

private Path getCachePath(String classifier) {
String version = getClass().getPackage().getImplementationVersion();
return Path.of(System.getProperty("user.home"),
".copilot", "runtime-cache", version, classifier, "runtime.node");
}
}
```

### 3. JNA loads from the extracted path

Once extracted to a known filesystem path, JNA loads it directly:

```java
NativeLibrary lib = NativeLibrary.getInstance(extractedPath.toString());
// Or via a mapped interface:
CopilotRuntime runtime = Native.load(extractedPath.toString(), CopilotRuntime.class);
```

### Key insight: the same code works in both modes

The beauty is that `getResourceAsStream("native/darwin-arm64/runtime.node")` works identically whether:

- The native lives in a **separate classifier JAR** on the classpath (normal dev dependency), OR
- It's been **merged into an uber-jar** by `maven-assembly-plugin`

The classloader doesn't care which JAR file the resource came from — it searches the entire classpath. This means **zero code changes** between the two consumption models.

---

## Assembly plugin configuration (consumer-side)

A consumer building a portable uber-jar would configure:

```xml
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
```

With all classifier JARs declared as dependencies:

```xml
<dependencies>
<dependency>
<groupId>com.github</groupId>
<artifactId>copilot-sdk-java</artifactId>
<version>${copilot.version}</version>
</dependency>
<!-- Include platforms you need -->
<dependency>
<groupId>com.github</groupId>
<artifactId>copilot-sdk-java-runtime</artifactId>
<version>${copilot.version}</version>
<classifier>linux-x64</classifier>
</dependency>
<dependency>
<groupId>com.github</groupId>
<artifactId>copilot-sdk-java-runtime</artifactId>
<version>${copilot.version}</version>
<classifier>darwin-arm64</classifier>
</dependency>
<!-- ... etc for each target platform -->
</dependencies>
```

---

## Why this works cleanly

| Concern | How it's handled |
|---------|-----------------|
| No resource path collisions | Each platform has its own subdirectory (`native/<classifier>/`) |
| Extraction only happens once | Cached to `~/.copilot/runtime-cache/<version>/<classifier>/` |
| Works without uber-jar too | Same `getResourceAsStream` call — classloader finds it in the separate JAR |
| Subset selection | Consumer declares only the classifiers they need; missing platforms get a clear error at runtime |
| JNA loading | `NativeLibrary.getInstance(path)` loads from an absolute filesystem path after extraction — no JNA platform-detection magic needed |

The pattern is identical to how DJL's `LibUtils.loadLibrary()` works — detect platform, construct resource path, extract if needed, load via absolute path.
Loading
Loading