Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 19 additions & 22 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Freenet Core – Agent Guide

@~/.claude/freenet-local.md

## Project Overview
Freenet Core is the peer-to-peer runtime that underpins applications in the Freenet ecosystem. The crates in this workspace implement the networking stack, contract execution environment, and developer tooling used by higher-level projects such as River.

Expand All @@ -8,20 +10,20 @@ Freenet Core is the peer-to-peer runtime that underpins applications in the Free
**BEFORE STARTING ANY WORK, verify your working directory:**

```bash
pwd # Must be ~/code/freenet/freenet-core/<branch-name>
# NOT ~/code/freenet/freenet-core/main
pwd # Must be <repo-root>/<branch-name>
# NOT <repo-root>/main
git branch --show-current # Should be your feature branch, NOT 'main'
```

### ❌ DO NOT work in ~/code/freenet/freenet-core/main
### ❌ DO NOT work in the main worktree directory

The main worktree should **always** remain on the `main` branch. Multiple agents working in main will conflict and corrupt branches.

### ✅ Creating a Worktree for Your Branch

1. Create a worktree as a sibling directory:
```bash
cd ~/code/freenet/freenet-core/main
cd <repo-root>/main # Your main checkout directory
git worktree add ../fix-<issue-number> <your-branch-name>
cd ../fix-<issue-number>
```
Expand Down Expand Up @@ -77,27 +79,22 @@ claude --permission-mode bypassPermissions

### Agent Spawning Template

**Note:** This template assumes you have terminal multiplexer tooling set up. Adapt paths and commands to your local environment.

```bash
# 1. Create/verify worktree
cd ~/code/freenet/freenet-core/main
cd <repo-root>/main
git worktree add ../fix-ISSUE-NUMBER branch-name

# 2. Create zellij tab (NO --layout flag!)
zellij action new-tab --name codex-iISSUE-description

# 3. Switch to mcp tab to avoid input corruption
zellij action go-to-tab-name mcp

# 4. Start agent with correct params
~/code/mcp/skills/zellij-agent-manager/scripts/send-to-agent.sh \
codex-iISSUE-description \
"cd ~/code/freenet/freenet-core/fix-ISSUE-NUMBER && codex -s danger-full-access -a never"
# 2. Start agent in new terminal/tab
# Navigate to worktree and launch with correct parameters
cd <repo-root>/fix-ISSUE-NUMBER
codex -s danger-full-access -a never
# OR: claude --permission-mode bypassPermissions

# 5. Wait for agent to start, then send task
sleep 3
~/code/mcp/skills/zellij-agent-manager/scripts/send-to-agent.sh \
codex-iISSUE-description \
"Your task description here [AI-assisted - Claude]"
# 3. Send task to agent
# (Use your terminal automation/multiplexer tooling)
# Example task: "Fix issue #XXXX - [description]. [AI-assisted - Claude]"
```

### Common Pitfalls
Expand Down Expand Up @@ -140,8 +137,8 @@ Run these in any worktree before pushing a branch or opening a PR.
- Never remove or ignore failing tests without understanding the root cause.

### Integration Testing with `freenet-test-network`
- Use the `freenet-test-network` crate located at `~/code/freenet/freenet-test-network` to spin up gateways and peers for integration tests.
- Add it as a dev-dependency in your worktree (`freenet-test-network = { path = "../freenet-test-network" }`) and construct networks with the builder API.
- Use the `freenet-test-network` crate from https://github.com/freenet/freenet-test-network to spin up gateways and peers for integration tests.
- Add it as a dev-dependency using either a path (if cloned locally) or git dependency, and construct networks with the builder API.
- Sample pattern:
```rust
use freenet_test_network::TestNetwork;
Expand Down
28 changes: 28 additions & 0 deletions crates/core/src/contract/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,21 @@ impl ContractHandlerChannel<SenderHalve> {
};
if let Err(e) = session_tx.try_send(msg) {
tracing::warn!("Failed to notify session actor: {}", e);
} else {
tracing::debug!(
%tx,
%client_id,
%request_id,
"Session adapter registered transaction with session actor"
);
}
}
} else {
tracing::warn!(
%client_id,
%request_id,
"Session adapter not installed; session actor will not track transaction"
);
}

Ok(())
Expand All @@ -365,7 +378,22 @@ impl ContractHandlerChannel<SenderHalve> {
};
if let Err(e) = session_tx.try_send(msg) {
tracing::warn!("Failed to notify session actor: {}", e);
} else {
tracing::debug!(
%tx,
%client_id,
%request_id,
contract = %contract_key,
"Session adapter registered subscription transaction with session actor"
);
}
} else {
tracing::warn!(
%client_id,
%request_id,
contract = %contract_key,
"Session adapter not installed; subscription transaction not registered with session actor"
);
}

Ok(())
Expand Down
80 changes: 34 additions & 46 deletions crates/core/src/node/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
//! network message processing from client notification logic, enabling
//! pure network processing when the actor system is enabled.

use crate::client_events::HostResult;
use crate::contract::SessionMessage;
use crate::message::Transaction;
use std::sync::Arc;
use tokio::sync::mpsc;
use tracing::{debug, error};
use tracing::debug;

/// Errors that can occur during message processing
#[derive(Debug, thiserror::Error)]
Expand All @@ -35,50 +36,43 @@ impl MessageProcessor {
pub async fn handle_network_result(
&self,
tx: Transaction,
op_result: Result<Option<crate::operations::OpEnum>, crate::node::OpError>,
host_result: Option<HostResult>,
) -> Result<(), ProcessingError> {
// Pure result forwarding to SessionActor
self.route_to_session_actor(tx, op_result).await
self.route_to_session_actor(tx, host_result).await
}

/// Route network result to SessionActor - no client parameters needed
async fn route_to_session_actor(
&self,
tx: Transaction,
op_result: Result<Option<crate::operations::OpEnum>, crate::node::OpError>,
host_result: Option<HostResult>,
) -> Result<(), ProcessingError> {
// Convert operation result to host result
let host_result = match op_result {
Ok(Some(op_res)) => {
debug!("Converting network result for transaction {}", tx);
Arc::new(op_res.to_host_result())
}
Ok(None) => {
debug!("No result to forward for transaction {}", tx);
return Ok(()); // No result to forward
}
Err(e) => {
error!("Network operation error for transaction {}: {}", tx, e);
// Create a generic client error for operation failures
use freenet_stdlib::client_api::{ClientError, ErrorKind};
Arc::new(Err(ClientError::from(ErrorKind::OperationError {
cause: e.to_string().into(),
})))
}
let status = match &host_result {
Some(Ok(_)) => "op_result",
Some(Err(_)) => "error",
None => "no_result",
};
tracing::debug!(%tx, status, "Routing network result to SessionActor");

let Some(host_result) = host_result else {
debug!("No result to forward for transaction {}", tx);
return Ok(());
};

// Create session message for pure actor routing
// The SessionActor will handle all client correlation internally
let session_msg = SessionMessage::DeliverHostResponse {
tx,
response: host_result,
response: Arc::new(host_result),
};

// Send to SessionActor - it handles all client concerns
if let Err(e) = self.result_tx.send(session_msg).await {
error!(
tracing::error!(
"Failed to send result to SessionActor for transaction {}: {}",
tx, e
tx,
e
);
return Err(ProcessingError::ActorCommunication(e));
}
Expand All @@ -94,37 +88,31 @@ impl MessageProcessor {
#[cfg(test)]
mod tests {
use super::*;
use crate::operations::{get, OpEnum};
use crate::operations::get;
use crate::operations::OpEnum;
use freenet_stdlib::prelude::ContractKey;
use tokio::sync::mpsc;

fn create_test_transaction() -> Transaction {
Transaction::new::<crate::operations::get::GetMsg>()
}

fn create_success_op_result() -> Result<Option<OpEnum>, crate::node::OpError> {
// Create a GetOp using the proper constructor
fn create_success_host_result() -> Option<HostResult> {
use freenet_stdlib::prelude::ContractInstanceId;
let key = ContractKey::from(ContractInstanceId::new([1u8; 32]));
let get_op = get::start_op(key, false, false);
Ok(Some(OpEnum::Get(get_op)))
Some(OpEnum::Get(get_op).to_host_result())
}

fn create_none_op_result() -> Result<Option<OpEnum>, crate::node::OpError> {
Ok(None)
fn create_none_host_result() -> Option<HostResult> {
None
}

fn create_error_op_result() -> Result<Option<OpEnum>, crate::node::OpError> {
let tx = create_test_transaction();
Err(crate::node::OpError::InvalidStateTransition {
tx,
#[cfg(debug_assertions)]
state: Some(
Box::new("test_state".to_string()) as Box<dyn std::fmt::Debug + Send + Sync>
),
#[cfg(debug_assertions)]
trace: std::backtrace::Backtrace::capture(),
})
fn create_error_host_result() -> Option<HostResult> {
use freenet_stdlib::client_api::{ClientError, ErrorKind};
Some(Err(ClientError::from(ErrorKind::OperationError {
cause: "test_state".into(),
})))
}

#[tokio::test]
Expand All @@ -143,7 +131,7 @@ mod tests {
let tx = create_test_transaction();

let result = processor
.handle_network_result(tx, create_success_op_result())
.handle_network_result(tx, create_success_host_result())
.await;

assert!(result.is_ok());
Expand All @@ -168,7 +156,7 @@ mod tests {
let tx = create_test_transaction();

let result = processor
.handle_network_result(tx, create_none_op_result())
.handle_network_result(tx, create_none_host_result())
.await;

assert!(result.is_ok());
Expand All @@ -191,7 +179,7 @@ mod tests {
let tx = create_test_transaction();

let result = processor
.handle_network_result(tx, create_error_op_result())
.handle_network_result(tx, create_error_host_result())
.await;

assert!(result.is_ok());
Expand Down Expand Up @@ -221,7 +209,7 @@ mod tests {
drop(session_rx);

let result = processor
.handle_network_result(tx, create_success_op_result())
.handle_network_result(tx, create_success_host_result())
.await;

assert!(result.is_err());
Expand Down
Loading
Loading