From 0824873ffb9f5438cdc7ed24ed00213dc08d32f3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 3 Dec 2025 12:14:32 +0000 Subject: [PATCH] Add /restart WhatsApp command to restart warelay --- CHANGELOG.md | 1 + src/auto-reply/reply.ts | 19 +++++++++++++++++++ src/infra/restart.ts | 14 ++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 src/infra/restart.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e42b159a..e92117730 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - **Pi/Tau stability:** RPC replies buffered until the assistant turn finishes; parsers return consistent `texts[]`; web auto-replies keep a warm Tau RPC process to avoid cold starts. - **Claude prompt flow:** One-time `sessionIntro` with per-message `/think:high` bodyPrefix; system prompt always sent on first turn even with `sendSystemOnce`. - **Heartbeat UX:** Backpressure skips reply heartbeats while other commands run; skips don’t refresh session `updatedAt`; web/Twilio heartbeats normalize array payloads and optional `heartbeatCommand`. +- **Control via WhatsApp:** Send `/restart` to restart the warelay launchd service (`com.steipete.warelay`) from your allowed numbers. - **Tau completion signal:** RPC now resolves on Tau’s `agent_end` (or process exit) so late assistant messages aren’t truncated; 5-minute hard cap only as a failsafe. ### Reliability & UX diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index 706a03bce..ebe65e97f 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -16,6 +16,7 @@ import { runCommandWithTimeout } from "../process/exec.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import type { TwilioRequester } from "../twilio/types.js"; import { sendTypingIndicator } from "../twilio/typing.js"; +import { triggerWarelayRestart } from "../infra/restart.js"; import { chunkText } from "./chunk.js"; import { runCommandReply } from "./command-reply.js"; import { @@ -25,6 +26,7 @@ import { } from "./templating.js"; import { isAudio, transcribeInboundAudio } from "./transcription.js"; import type { GetReplyOptions, ReplyPayload } from "./types.js"; +import { triggerWarelayRestart } from "../infra/restart.js"; export type { GetReplyOptions, ReplyPayload } from "./types.js"; @@ -347,6 +349,11 @@ export async function getReplyFromConfig( const to = (ctx.To ?? "").replace(/^whatsapp:/, ""); const isSamePhone = from && to && from === to; const abortKey = sessionKey ?? (from || undefined) ?? (to || undefined); + const rawBodyNormalized = ( + sessionCtx.BodyStripped ?? sessionCtx.Body ?? "" + ) + .trim() + .toLowerCase(); if (!sessionEntry && abortKey) { abortedLastRun = ABORT_MEMORY.get(abortKey) ?? false; @@ -366,6 +373,18 @@ export async function getReplyFromConfig( } } + if ( + rawBodyNormalized === "/restart" || + rawBodyNormalized === "restart" || + rawBodyNormalized.startsWith("/restart ") + ) { + triggerWarelayRestart(); + cleanupTyping(); + return { + text: "Restarting warelay via launchctl; give me a few seconds to come back online.", + }; + } + const abortRequested = reply?.mode === "command" && isAbortTrigger((sessionCtx.BodyStripped ?? sessionCtx.Body ?? "").trim()); diff --git a/src/infra/restart.ts b/src/infra/restart.ts new file mode 100644 index 000000000..cd6cfc720 --- /dev/null +++ b/src/infra/restart.ts @@ -0,0 +1,14 @@ +import { spawn } from "node:child_process"; + +const DEFAULT_LAUNCHD_LABEL = "com.steipete.warelay"; + +export function triggerWarelayRestart(): void { + const label = process.env.WARELAY_LAUNCHD_LABEL || DEFAULT_LAUNCHD_LABEL; + const uid = typeof process.getuid === "function" ? process.getuid() : undefined; + const target = uid !== undefined ? `gui/${uid}/${label}` : label; + const child = spawn("launchctl", ["kickstart", "-k", target], { + detached: true, + stdio: "ignore", + }); + child.unref(); +}