Add /restart WhatsApp command to restart warelay

This commit is contained in:
Peter Steinberger
2025-12-03 12:14:32 +00:00
parent 8f99b13305
commit 0824873ffb
3 changed files with 34 additions and 0 deletions

View File

@@ -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 dont 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 Taus `agent_end` (or process exit) so late assistant messages arent truncated; 5-minute hard cap only as a failsafe.
### Reliability & UX

View File

@@ -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());

14
src/infra/restart.ts Normal file
View File

@@ -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();
}