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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Code and comments are in English. The code must read as if an experienced produc

### Comments

- These rules apply to every language in the repo, not just C++: `//` in C++, Swift, Kotlin, and Dart, and `#` in CMake, Makefile, YAML, and shell all follow the same standard. No `====`/`----` banner rows and no capitalized section headers anywhere, including build files.
- Comments are rare and used only where naming cannot carry the meaning. If it is obvious, decorative, or restates the code, delete it.
- A comment explains intent or context, not the literal code.
- Never add artificial section separators like `helpers`, `validators`, or `public methods`. Never write header comments that describe a method, a member, or a section.
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ if(IONCLAW_BUILD_SHARED)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif()

# Apple platform deployment targets
# apple platform deployment targets
if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
set(CMAKE_OSX_DEPLOYMENT_TARGET "15.0" CACHE STRING "iOS deployment target")
elseif(CMAKE_SYSTEM_NAME STREQUAL "watchOS")
Expand Down
36 changes: 10 additions & 26 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# IonClaw - Build System
# Usage: make help
# ionclaw build system
# run make help for the list of targets

.DEFAULT_GOAL := help

Expand Down Expand Up @@ -45,9 +45,7 @@ ANDROID_ABIS := arm64-v8a armeabi-v7a x86_64 x86
# --- helpers ---
NPROC := $(shell nproc 2>/dev/null || sysctl -n hw.ncpu)

# ============================================================
# Help
# ============================================================
# help

.PHONY: help
help:
Expand Down Expand Up @@ -117,9 +115,7 @@ help:
@echo " make build-android ANDROID_NDK=/path/to/ndk"
@echo ""

# ============================================================
# Build
# ============================================================
# build

.PHONY: build
build: ## Build server executable (release)
Expand Down Expand Up @@ -384,9 +380,7 @@ build-docker: ## Build Docker image
docker build -t ionclaw .
@echo "==> Done: docker image 'ionclaw'"

# ============================================================
# Run
# ============================================================
# run

.PHONY: run
run: build ## Build and run server (release)
Expand Down Expand Up @@ -429,9 +423,7 @@ run-android: build-android-aar ## Build (if needed), install and launch the nati
adb shell am start -n com.ionclaw.app/.MainActivity
@echo "==> Done."

# ============================================================
# Setup
# ============================================================
# setup

.PHONY: setup-web
setup-web: ## Install web client npm dependencies
Expand Down Expand Up @@ -495,9 +487,7 @@ link-android: build-android-arm64 build-android-x86_64 ## Copy Android .so files
cp $$(find $(ANDROID_NDK)/toolchains/llvm/prebuilt -path '*/lib/linux/x86_64/libomp.so' | head -1) $(ANDROID_AAR_JNILIBS)/x86_64/libomp.so
@echo "==> Done: $(ANDROID_AAR_JNILIBS)/"

# ============================================================
# Release
# ============================================================
# release

.PHONY: release-macos
release-macos: link-flutter-macos link-flutter-web ## Build macOS release
Expand Down Expand Up @@ -534,9 +524,7 @@ android-gen-key: ## Generate Android upload keystore and certificate
flutter-deps: ## Install and upgrade Flutter dependencies
cd $(FLUTTER_RUNNER_DIR) && flutter pub get && flutter pub upgrade

# ============================================================
# Clean
# ============================================================
# clean

.PHONY: clean
clean: ## Remove all build directories
Expand Down Expand Up @@ -569,9 +557,7 @@ clean-android: ## Remove Android build directories and jniLibs
rm -rf $(ANDROID_AAR_JNILIBS)
rm -rf $(BUILD_ANDROID_AAR)

# ============================================================
# Version
# ============================================================
# version

.PHONY: version
version: ## Set project version: make version V=1.0.0+2
Expand All @@ -597,9 +583,7 @@ version: ## Set project version: make version V=1.0.0+2
echo " web/sw.js → $$VER"; \
echo "==> Done."

# ============================================================
# Code
# ============================================================
# code

.PHONY: format
format: ## Format all C/C++/ObjC sources with clang-format
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
Expand Down
5 changes: 2 additions & 3 deletions apps/apple/Shared/Sources/Platform/PlatformBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import Foundation
import UserNotifications

// bridges the native invoke_platform tool to apple platform features.
// the agent calls invoke_platform("local-notification.send", {title, message}) and the core
// forwards it here via the async c callback; we must respond exactly once per request.
// the core forwards each invoke_platform call here via the async c callback, and we respond exactly once per request.
final class IonClawPlatform: NSObject, UNUserNotificationCenterDelegate {
static let shared = IonClawPlatform()

Expand Down Expand Up @@ -36,7 +35,7 @@ final class IonClawPlatform: NSObject, UNUserNotificationCenterDelegate {
UNUserNotificationCenter.current().requestAuthorization(options: options) { _, _ in }
}

// dispatches a request coming from the native core. runs on a core thread and must not block;
// dispatches a request from the native core on a core thread and must not block.
// the response is delivered later from the async completion handler.
func handle(requestId: Int64, function: String, paramsJson: String) {
switch function {
Expand Down
2 changes: 1 addition & 1 deletion apps/apple/Shared/Sources/UI/QRCode.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

// pure-swift qr code generator (byte mode), so it works on watchos where coreimage is unavailable.
// follows the qr code model 2 specification (iso/iec 18004); algorithm based on project nayuki (mit).
// follows the qr code model 2 specification (iso/iec 18004), algorithm based on project nayuki (mit).
struct QRCode {
enum Correction: Int {
case low = 0
Expand Down
2 changes: 1 addition & 1 deletion apps/flutter/runner/lib/screen/browser_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class _BrowserScreenState extends State<BrowserScreen> {
return NavigationDecision.prevent;
}

// Intercept file download URLs
// intercept file download urls
if (uri.path.startsWith('/api/files/download/')) {
_downloadFile(uri);
return NavigationDecision.prevent;
Expand Down
19 changes: 6 additions & 13 deletions cmake/embed-resources.cmake
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
# embed-resources.cmake
# Creates a ZIP of SOURCE_DIR and generates C++ source/header with the data as a byte array.
#
# Input variables:
# SOURCE_DIR - directory to embed
# OUTPUT_CPP - output .cpp file path
# OUTPUT_HPP - output .hpp file path
# NAMESPACE - C++ namespace for the generated code
# zips SOURCE_DIR and generates a c++ source and header embedding the bytes under NAMESPACE

if(NOT SOURCE_DIR OR NOT OUTPUT_CPP OR NOT OUTPUT_HPP OR NOT NAMESPACE)
message(FATAL_ERROR "embed-resources.cmake requires SOURCE_DIR, OUTPUT_CPP, OUTPUT_HPP, NAMESPACE")
Expand All @@ -15,7 +8,7 @@ if(NOT EXISTS "${SOURCE_DIR}")
message(FATAL_ERROR "embed-resources.cmake: SOURCE_DIR does not exist: ${SOURCE_DIR}")
endif()

# create ZIP archive
# create the zip archive
set(ZIP_FILE "${OUTPUT_CPP}.zip")
execute_process(
COMMAND ${CMAKE_COMMAND} -E tar cf "${ZIP_FILE}" --format=zip -- .
Expand All @@ -27,16 +20,16 @@ if(NOT ZIP_RESULT EQUAL 0)
message(FATAL_ERROR "embed-resources.cmake: failed to create ZIP from ${SOURCE_DIR}")
endif()

# read ZIP as hex
# read the zip as hex
file(READ "${ZIP_FILE}" ZIP_HEX HEX)
file(SIZE "${ZIP_FILE}" ZIP_SIZE)

# format as C byte array: "0xAB,0xCD,..."
# format as a c byte array like 0xAB,0xCD
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," HEX_ARRAY "${ZIP_HEX}")

# generate .cpp
file(WRITE "${OUTPUT_CPP}"
"// Auto-generated by embed-resources.cmake - DO NOT EDIT
"// generated by embed-resources.cmake, do not edit
#include <cstddef>

namespace ${NAMESPACE} {
Expand All @@ -51,7 +44,7 @@ size_t getSize() { return ${ZIP_SIZE}; }

# generate .hpp
file(WRITE "${OUTPUT_HPP}"
"// Auto-generated by embed-resources.cmake - DO NOT EDIT
"// generated by embed-resources.cmake, do not edit
#pragma once
#include <cstddef>

Expand Down
2 changes: 1 addition & 1 deletion main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ set(IONCLAW_LIB_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/lib/src/ionclaw/server/handler/McpHandler.cpp
)

# --- embed resources (shared by server and shared library) ---
# embed resources shared by the server and the shared library
# web client
file(GLOB_RECURSE IONCLAW_WEB_FILES "${CMAKE_CURRENT_SOURCE_DIR}/resources/web/*")
add_custom_command(
Expand Down
2 changes: 1 addition & 1 deletion main/lib/include/ionclaw/bus/MessageBus.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class MessageBus
std::condition_variable inboundCv;
std::condition_variable outboundCv;

// bounds inbound memory under a flood; a burst beyond this is rejected with a clear error rather than buffered without limit
// bounds inbound memory under a flood by rejecting a burst beyond this with a clear error instead of buffering without limit
static constexpr size_t MAX_INBOUND_QUEUE = 1000;

// bounds each channel's outbound backlog so a stalled runner cannot grow it without limit
Expand Down
2 changes: 1 addition & 1 deletion main/lib/include/ionclaw/channel/TelegramRunner.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class TelegramRunner
void sendTextMessage(const std::string &chatId, const std::string &text, int replyToMessageId = 0);
void sendChunkedMessage(const std::string &chatId, const std::string &text, int replyToMessageId = 0);

// sends a media attachment by public url; kind is image|audio|video|document
// sends a media attachment by public url with kind image, audio, video, or document
void sendMediaByUrl(const std::string &chatId, const std::string &kind, const std::string &url, const std::string &caption, int replyToMessageId = 0);

// turns a workspace media path into its public url, passing absolute urls through unchanged
Expand Down
2 changes: 1 addition & 1 deletion main/lib/include/ionclaw/channel/WebhookDedup.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace channel
class WebhookDedup
{
public:
// returns true if this id is new (process it); false if it was already seen within the retention window
// returns true if this id is new and false if it was already seen within the retention window
bool markSeen(const std::string &msgId);

size_t size() const;
Expand Down
4 changes: 2 additions & 2 deletions main/lib/include/ionclaw/channel/WhatsAppSender.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ class WhatsAppSender
// z-api base url is https://api.z-api.io/instances/{id}/token/{token}; client token goes in a header on every call
static OutboundRequest zApiSendText(const std::string &baseUrl, const std::string &clientToken, const std::string &phone, const std::string &text, int delayTyping = 3);

// z-api media fields accept either a base64 data url or a public url; kind is image|audio|video|document
// z-api media fields accept a base64 data url or a public url with kind image, audio, video, or document
static OutboundRequest zApiSendMedia(const std::string &baseUrl, const std::string &clientToken, const std::string &phone, const std::string &kind, const std::string &dataUrl, const std::string &caption, const std::string &fileName);

// meta graph endpoint is https://graph.facebook.com/{version}/{phoneNumberId}/messages with a bearer token
static OutboundRequest metaSendText(const std::string &graphVersion, const std::string &phoneNumberId, const std::string &accessToken, const std::string &to, const std::string &text);

// meta sends media by a public link; kind is image|audio|video|document, caption applies to everything except audio
// meta sends media by a public link where the caption applies to everything except audio
static OutboundRequest metaSendMedia(const std::string &graphVersion, const std::string &phoneNumberId, const std::string &accessToken, const std::string &to, const std::string &kind, const std::string &link, const std::string &caption, const std::string &fileName);

// meta marks the last inbound message read and shows a typing indicator in one call
Expand Down
6 changes: 3 additions & 3 deletions main/lib/include/ionclaw/channel/WhatsAppWebhook.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ namespace channel
struct WebhookMedia
{
std::string kind; // image | audio | video | document
std::string url; // direct download url (z-api); empty for meta
std::string mediaId; // media id to fetch in two steps (meta); empty for z-api
std::string url; // direct download url for z-api, empty for meta
std::string mediaId; // media id fetched in two steps for meta, empty for z-api
std::string mimeType;
std::string caption;
std::string fileName; // documents only
Expand All @@ -43,7 +43,7 @@ class WhatsAppWebhook
// z-api posts a single message object per webhook call
static ParsedWebhookMessage parseZApi(const nlohmann::json &body);

// meta wraps messages under entry[].changes[].value.messages[]; one call may carry several
// meta wraps messages under entry[].changes[].value.messages[] and one call may carry several
static std::vector<ParsedWebhookMessage> parseMeta(const nlohmann::json &body);

// validates the meta X-Hub-Signature-256 header (sha256=<hex>) against hmac-sha256(rawBody, appSecret)
Expand Down
2 changes: 1 addition & 1 deletion main/lib/include/ionclaw/provider/ModelCapabilities.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class ModelCapabilities
// returns the record for a model (exact key, then after stripping a provider/ prefix), or nullopt if unknown
static std::optional<ModelInfo> lookup(const std::string &model);

// capability queries; an unknown model defaults to permissive so a valid but unlisted model is never silently degraded
// capability queries default an unknown model to permissive so a valid but unlisted model is never silently degraded
static bool supportsVision(const std::string &model, bool defaultIfUnknown = true);
static bool supportsReasoning(const std::string &model, bool defaultIfUnknown = true);
static bool supportsFunctionCalling(const std::string &model, bool defaultIfUnknown = true);
Expand Down
2 changes: 1 addition & 1 deletion main/lib/include/ionclaw/server/Routes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class Routes

void handleAuthLogin(Poco::Net::HTTPServerRequest &req, Poco::Net::HTTPServerResponse &resp);

// inbound whatsapp webhooks (unauthenticated; verified by provider signature/token)
// inbound whatsapp webhooks that are unauthenticated but verified by the provider signature or token
void handleWhatsAppZApiWebhook(Poco::Net::HTTPServerRequest &req, Poco::Net::HTTPServerResponse &resp);
void handleWhatsAppMetaWebhook(Poco::Net::HTTPServerRequest &req, Poco::Net::HTTPServerResponse &resp);

Expand Down
2 changes: 1 addition & 1 deletion main/lib/src/ionclaw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ extern "C"
return jsonToMalloc({{"success", false}, {"error", "failed to extract template"}});
}

// orchestrator state lives at the project root; the agent workspace holds the agent's own files, including public/
// orchestrator state lives at the project root while the agent workspace holds the agent's own files under public/
std::filesystem::create_directories(targetDir + "/sessions", ec);
std::filesystem::create_directories(targetDir + "/workspace/skills", ec);
std::filesystem::create_directories(targetDir + "/workspace/memory", ec);
Expand Down
22 changes: 8 additions & 14 deletions main/lib/src/ionclaw/agent/AgentLoop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ nlohmann::json AgentLoop::resolveMedia(const std::vector<std::string> &paths, co

for (const auto &path : paths)
{
// only process audio files for transcription; images are handled via vision tool
// only audio files are transcribed here, images go through the vision tool
auto ext = fs::path(path).extension().string();
ionclaw::util::StringHelper::toLowerInPlace(ext);

Expand Down Expand Up @@ -404,8 +404,7 @@ void UsageTracker::record(const nlohmann::json &usage)
return;
}

// a proxy may send a token field as null (e.g. cache_creation_input_tokens when caching is unused), and value()
// throws type_error.302 on an explicit null, so read each field only when it is actually an integer
// a proxy may send a token field as an explicit null, so read each field only when it is actually an integer
// clang-format off
auto readInt = [&usage](const char *key) {
auto it = usage.find(key);
Expand Down Expand Up @@ -471,9 +470,8 @@ void AgentLoop::processMessage(const ionclaw::bus::InboundMessage &message, cons
auto sessionKey = (message.metadata.contains("agent_session_key") && message.metadata["agent_session_key"].is_string()) ? message.metadata["agent_session_key"].get<std::string>() : message.sessionKey();
auto baseKey = message.sessionKey();

// per-turn state lives on the stack; the active-turn handle and session queue are still shared members, so this is
// safe only because the orchestrator serializes turns on one worker thread and never runs two turns on one AgentLoop
// at once. enabling real per-agent concurrency would require threading these through TurnState instead of the members.
// per-turn state lives on the stack and is safe only because the orchestrator serializes turns on one worker thread.
// per-agent concurrency would require threading the shared active-turn handle and session queue through TurnState.
TurnState turnState;
turnState.sessionQueuePtr = defaultSessionQueuePtr.load(std::memory_order_acquire);
turnState.activeTurnHandle = defaultActiveTurnHandle.load(std::memory_order_acquire);
Expand Down Expand Up @@ -643,10 +641,8 @@ void AgentLoop::processMessage(const ionclaw::bus::InboundMessage &message, cons
}
}

// save user message to the agent-scoped session
// channels pre-save under base key for immediate persistence; we also persist
// under the agent-scoped key so each agent has its own conversation history
// skip synthetic messages (steer injections, wake prompts) — not real user input
// channels pre-save under the base key, and this also persists under the agent-scoped key so each agent keeps its own history
// skip synthetic messages like steer injections and wake prompts that are not real user input
bool isSynthetic = message.metadata.contains("synthetic") && message.metadata["synthetic"].is_boolean() && message.metadata["synthetic"].get<bool>();

if (!isSynthetic)
Expand Down Expand Up @@ -759,8 +755,7 @@ void AgentLoop::processMessage(const ionclaw::bus::InboundMessage &message, cons
// run agent loop
auto responseText = runAgentLoop(messages, taskId, baseKey, sessionKey, agentName, toolContext, callback, turnState);

// resolve empty/silent responses: prefer message-tool content from this turn,
// then fall back to last sent content from a previous turn
// for an empty or silent response prefer message-tool content from this turn, then the last sent content from a previous turn
if (responseText == "[SILENT]")
{
responseText = !messageToolDeliveredContent.empty() ? messageToolDeliveredContent : !turnState.lastSentContent.empty() ? turnState.lastSentContent
Expand Down Expand Up @@ -1437,8 +1432,7 @@ std::string AgentLoop::runAgentLoop(std::vector<ionclaw::provider::Message> &mes
}
}

// assemble final text: use last iteration's content if non-empty,
// otherwise fall back to accumulated text from all iterations
// use the last iteration's content when non-empty, otherwise the text accumulated across all iterations
std::string finalText = response.content;

if (finalText.empty() && iteration > 0)
Expand Down
Loading
Loading