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
15 changes: 11 additions & 4 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ tasks:
desc: First-time setup (install deps, build, link, run setup wizard)
cmds:
- task: check-tools
- bun install --force
- npx --yes inngest-cli@latest --version
- cmd: bun install --force
silent: true
- cmd: npx --yes inngest-cli@latest --version
silent: true
- task: install
- geminiclaw setup

install:
desc: Build and install geminiclaw globally (bun link)
silent: true
sources:
- src/**/*.ts
- templates/**/*
Expand All @@ -44,10 +47,11 @@ tasks:

start:
desc: "Start all services (inngest + serve). Use -d for daemon: task start -- -d"
silent: true
cmds:
- task: install
- rm -f .overmind.sock
- find /tmp/tmux-$(id -u)/ -name "overmind-geminiclaw-*" -type s -delete 2>/dev/null || true
- cmd: rm -f .overmind.sock
- cmd: find /tmp/tmux-$(id -u)/ -name "overmind-geminiclaw-*" -type s -delete 2>/dev/null || true
- |
if echo "{{.CLI_ARGS}}" | grep -q -- '-d'; then
overmind start -D
Expand All @@ -61,12 +65,15 @@ tasks:

stop:
desc: Stop all services (gracefully quit overmind daemon)
silent: true
cmd: overmind quit

status:
desc: Show service status
silent: true
cmd: overmind status

logs:
desc: Tail all service logs (Ctrl-C to quit)
silent: true
cmd: overmind echo
8 changes: 3 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"@google/gemini-cli": "0.34.0-nightly.20260307.6c3a90645",
"@mariozechner/pi-tui": "^0.54.2",
"@modelcontextprotocol/sdk": "^1",
"@tobilu/qmd": "^1.0.7",
"@tobilu/qmd": "^1.1.6",
"chalk": "^5",
"chat": "^4.14.0",
"commander": "^12",
Expand Down Expand Up @@ -77,7 +77,7 @@
"templates/"
],
"patchedDependencies": {
"@tobilu/qmd@1.0.7": "patches/@tobilu%2Fqmd@1.0.7.patch",
"@tobilu/qmd@1.1.6": "patches/@tobilu%2Fqmd@1.1.6.patch",
"@google/gemini-cli@0.34.0-nightly.20260307.6c3a90645": "patches/@google%2Fgemini-cli@0.34.0-nightly.20260307.6c3a90645.patch"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
diff --git a/bundle/gemini.js b/bundle/gemini.js
index ea0166058a9bacdd0311e085cb112df5d6828786..72bc2eef5d0e9879cded610e9615984904a1b9e7 100755
index ea0166058a9bacdd0311e085cb112df5d6828786..29daac5f364b13e54276d0546426488b55903c2e 100755
--- a/bundle/gemini.js
+++ b/bundle/gemini.js
@@ -424765,7 +424765,7 @@ var init_local_executor = __esm({
Expand Down Expand Up @@ -29,7 +29,15 @@ index ea0166058a9bacdd0311e085cb112df5d6828786..72bc2eef5d0e9879cded610e96159849
}
}
let nextMessage = { role: "user", parts: parts2 };
@@ -571191,7 +571191,7 @@ ${thought.description}`;
@@ -571184,14 +571184,14 @@ ${thought.description}`;
await this.config.getGemini31Launched?.() ?? false
);
const responseStream = await chat.sendMessageStream(
- { model },
+ { model, isChatModel: true },
nextMessage?.parts ?? [],
promptId,
pendingSend.signal,
LlmRole.MAIN
);
nextMessage = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
diff --git a/dist/llm.js b/dist/llm.js
index 6e9fa7dc6c7590ac28184196b88d391365aca433..7297b7ba616862c5536f585198cd1cf81abdf3ce 100644
index b304389c0bc299477cd887c6f73c3266e2f1c8b6..bc91c1b94c3840c07af65edc9b36216775654739 100644
--- a/dist/llm.js
+++ b/dist/llm.js
@@ -461,7 +461,7 @@ export class LlamaCpp {
@@ -485,7 +485,7 @@ export class LlamaCpp {
// Qwen3 reranker template adds ~200 tokens overhead (system prompt, tags, etc.)
// Chunks are max 800 tokens, so 800 + 200 + query ≈ 1100 tokens typical.
// Use 2048 for safety margin. Still 17× less than auto (40960).
Expand All @@ -24,10 +24,10 @@ index fd430fa49ba2785151289337c2c02d830a7f1b68..67519ba8fc86386be0ca1bbe5a7e018b
export type HttpServerHandle = {
httpServer: import("http").Server;
diff --git a/dist/mcp.js b/dist/mcp.js
index 7484edf93601d273b4b504679518274a777b37f7..efe7081d2ddcd72b5c22c7ce8573a877dc9dc44b 100644
index d1b09687ae86c1f22c245091ed7391879057f7c1..29946f0f109449d7d36e071b46cfdfbf01e48980 100644
--- a/dist/mcp.js
+++ b/dist/mcp.js
@@ -100,7 +100,7 @@ function buildInstructions(store) {
@@ -108,7 +108,7 @@ function buildInstructions(store) {
* Create an MCP server with all QMD tools, resources, and prompts registered.
* Shared by both stdio and HTTP transports.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/agent/context-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ describe('ContextBuilder', () => {
it('includes memory guidelines with QMD search reference', async () => {
const content = await builder.writeStaticGeminiMd();
expect(content).toContain('## Memory Management');
expect(content).toContain('qmd_search');
expect(content).toContain('qmd_query');
});

it('includes autonomy level restrictions when not autonomous', async () => {
Expand Down
37 changes: 28 additions & 9 deletions src/agent/context-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export interface SessionContextOptions {
bootstrap?: boolean;
/** IANA timezone for timestamp formatting. Falls back to system timezone. */
timezone?: string;
/** Delivery target for cron jobs — `platform:channelId` (e.g. "discord:123456"). */
deliveryTarget?: string;
}

// ── Truncation helper ────────────────────────────────────────────
Expand All @@ -69,6 +71,16 @@ export function truncateWithContext(content: string, maxChars: number): string {
export class ContextBuilder {
constructor(private workspaceRoot: string) {}

/** Resolve home channel as `platform:channelId` string. */
private resolveHomeChannel(): string | undefined {
const config = loadConfig();
const dc = config.channels.discord;
if (dc.enabled && dc.homeChannel) return `discord:${dc.homeChannel}`;
const sc = config.channels.slack;
if (sc.enabled && sc.homeChannel) return `slack:${sc.homeChannel}`;
return undefined;
}

/**
* Check whether GEMINI.md already exists in the workspace.
*/
Expand Down Expand Up @@ -157,7 +169,7 @@ export class ContextBuilder {
);
parts.push('');
parts.push(
'To review recent activity, use `qmd_search` for keyword search or `qmd_deep_search` for hybrid search across daily logs and memory files, then `qmd_get` to drill into results.',
'To review recent activity, use `qmd_query` for hybrid search across daily logs and memory files, then `qmd_get` to drill into results.',
);
parts.push('');

Expand All @@ -166,7 +178,7 @@ export class ContextBuilder {
if (autonomyLevel === 'read_only') {
parts.push('## Restriction: READ_ONLY Mode');
parts.push('**This session is limited to read-only operations.**');
parts.push('- Allowed: file reads, searches, information gathering, `qmd_search`, `qmd_get`');
parts.push('- Allowed: file reads, searches, information gathering, `qmd_query`, `qmd_get`');
parts.push('- Prohibited: file writes/deletes, shell command execution, form submissions');
parts.push('- If asked to perform a prohibited operation, explain the restriction and decline');
parts.push('');
Expand Down Expand Up @@ -275,13 +287,9 @@ export class ContextBuilder {
parts.push('**Rotate (every few hours):** Memory maintenance, proactive background work.');

// Inject home channel so the agent knows where to post notifications
const config = loadConfig();
const dc = config.channels.discord;
const sc = config.channels.slack;
if (dc.enabled && dc.homeChannel) {
parts.push(`Home channel: discord:${dc.homeChannel}`);
} else if (sc.enabled && sc.homeChannel) {
parts.push(`Home channel: slack:${sc.homeChannel}`);
const homeChannel = this.resolveHomeChannel();
if (homeChannel) {
parts.push(`Home channel: ${homeChannel}`);
}

parts.push('');
Expand All @@ -296,6 +304,17 @@ export class ContextBuilder {
parts.push('Focus exclusively on the prompt and produce the requested output.');
parts.push('Do NOT run background heartbeat checks.');
parts.push('Do NOT respond with HEARTBEAT_OK.');

// Inject delivery target so the agent knows where to post results
const target = options.deliveryTarget ?? this.resolveHomeChannel();
if (target) {
parts.push('');
parts.push(`Delivery target: ${target}`);
parts.push(
'Post results via `geminiclaw_post_message` to the delivery target. ' +
'Do NOT output results as plain response text — use the tool for delivery.',
);
}
} else {
parts.push('');
parts.push('### Interactive Mode');
Expand Down
22 changes: 20 additions & 2 deletions src/agent/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const messageDeltaEvent: MessageEvent = {

const toolUseEvent: ToolUseEvent = {
type: 'tool_use',
tool_name: 'qmd__qmd_search',
tool_name: 'qmd__qmd_query',
tool_id: 'tool-1',
parameters: { text: 'user name is Test', category: 'user' },
timestamp: '2026-02-22T10:00:03Z',
Expand Down Expand Up @@ -124,7 +124,7 @@ describe('RunResultBuilder', () => {

expect(result.toolCalls).toHaveLength(1);
expect(result.toolCalls[0].id).toBe('tool-1');
expect(result.toolCalls[0].name).toBe('qmd__qmd_search');
expect(result.toolCalls[0].name).toBe('qmd__qmd_query');
expect(result.toolCalls[0].args).toEqual({
text: 'user name is Test',
category: 'user',
Expand Down Expand Up @@ -192,6 +192,24 @@ describe('RunResultBuilder', () => {
expect(result.trigger).toBe('heartbeat');
});

it('detects HEARTBEAT_OK appended to end of line without newline', () => {
const builder = new RunResultBuilder('heartbeat');
builder.handleEvent({
...messageEvent,
content: 'Updated heartbeat-state.json.HEARTBEAT_OK',
});
expect(builder.build().heartbeatOk).toBe(true);
});

it('detects HEARTBEAT_OK wrapped in markdown bold', () => {
const builder = new RunResultBuilder('heartbeat');
builder.handleEvent({
...messageEvent,
content: '**HEARTBEAT_OK**',
});
expect(builder.build().heartbeatOk).toBe(true);
});

it('heartbeatOk is false when not present', () => {
const builder = new RunResultBuilder();
builder.handleEvent(messageEvent);
Expand Down
7 changes: 5 additions & 2 deletions src/agent/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,11 @@ export class RunResultBuilder {
// Strip <think>...</think> and <thought>...</thought> reasoning blocks before checking —
// the HEARTBEAT_OK signal must come from the actual response, not internal reasoning.
const responseWithoutThink = this.result.responseText.replace(/<(think|thought)>[\s\S]*?<\/\1>/g, '');
// HEARTBEAT_OK must appear as a standalone line, not embedded in prose.
this.result.heartbeatOk = /^\s*HEARTBEAT_OK\s*$/m.test(responseWithoutThink);
// Detect HEARTBEAT_OK: strip markdown decoration, then match at end of any line.
// LLMs sometimes append the token without a preceding newline or wrap it in
// bold/code markers — normalising these avoids false negatives (à la OpenClaw).
const normalized = responseWithoutThink.replace(/[*`~]/g, '');
this.result.heartbeatOk = /HEARTBEAT_OK\s*$/m.test(normalized);

// Safety net: if extractPendingSkillActivation() was never called by the caller,
// parse all skill activations from the response text now.
Expand Down
5 changes: 1 addition & 4 deletions src/agent/turn/finalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ function isBackgroundJob(ctx: DeliverContext): boolean {
}

function hasReplyTarget(ctx: DeliverContext): boolean {
if (!ctx.eventData.serializedThread && !ctx.eventData.reply) return false;
// Heartbeat OK doesn't need a reply — execution log channel is sufficient
if (ctx.eventData.trigger === 'heartbeat' && ctx.runResult.heartbeatOk) return false;
return true;
return !!ctx.eventData.serializedThread;
}

async function generateTitle(ctx: DeliverContext): Promise<void> {
Expand Down
2 changes: 2 additions & 0 deletions src/agent/turn/pre-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export async function buildAgentContext(
| 'channelContextMaxChars'
| 'isHomeChannel'
| 'isDM'
| 'deliveryTarget'
>,
): Promise<{ sessionContext: string }> {
const builder = new ContextBuilder(params.workspacePath);
Expand Down Expand Up @@ -131,6 +132,7 @@ export async function buildAgentContext(
channelContext,
bootstrap,
timezone: params.timezone,
deliveryTarget: params.deliveryTarget,
});

return { sessionContext };
Expand Down
14 changes: 5 additions & 9 deletions src/agent/turn/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,10 @@ export interface AgentRunEventData {
*/
serializedThread?: string;
/**
* Legacy reply info — kept for cron jobs and non-Chat-SDK triggers (heartbeat, manual).
* When serializedThread is present, this field is ignored.
* Delivery target for cron jobs — `platform:channelId` string (e.g. "discord:123456").
* Injected into agent context so skills know where to post via geminiclaw_post_message.
*/
reply?: {
/** Adapter identifier — matches ChannelAdapter.channelType (e.g. 'discord'). */
channelType: string;
channelId: string;
/** Threading reference passed to sendReply as opts.replyRef. */
replyRef?: string;
};
deliveryTarget?: string;
/** Discord/Slack channel topic (description). Injected into session context for per-channel behavior control. */
channelTopic?: string;
/** Recent channel conversation context (messages + thread summaries). Serialized ChannelContextData. */
Expand Down Expand Up @@ -79,6 +73,8 @@ export interface RunTurnParams {
channelContext?: string;
/** Max chars for rendered channel context block. Sourced from config.experimental.channelContext.maxChars. */
channelContextMaxChars?: number;
/** Delivery target for cron jobs — `platform:channelId` (e.g. "discord:123456"). */
deliveryTarget?: string;
/** Sandbox mode: true (auto-detect), false (disabled), 'seatbelt', or 'docker'. */
sandbox?: SandboxMode;
/** Internal flag to prevent infinite retry on context overflow. */
Expand Down
20 changes: 0 additions & 20 deletions src/channels/reply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,26 +153,6 @@ export async function deliverReply(opts: {
log.info('reply sent via Chat SDK', { totalMs: Date.now() - t0, postMs: Date.now() - tChatDone });
return;
}

// Legacy path: use postToChannel for cron/heartbeat replies
const { reply } = eventData;
if (!reply) return;

log.info('sending reply (legacy)', {
channelType: reply.channelType,
channelId: reply.channelId,
chars: replyText.length,
mediaItems: mediaItems.length,
});

await postToChannel({
channelType: reply.channelType as 'discord' | 'slack',
channelId: reply.channelId,
threadRef: reply.replyRef,
text: replyText,
files: mediaItems,
config,
});
}

/**
Expand Down
Loading
Loading