A Rust backend that owns the sessions and an Android app that reads them. The server spawns and adopts CLI processes, normalises everything they emit into one event model, keeps the transcript, and serves it over pinned TLS on a WireGuard interface; the phone streams that, replies, sends images, and imports conversations the machine already has. `AGENTS.md` is the working guide -- what runs where, what has been measured, and the faults that were expensive to find. `PLAN.md` is the design record. History before this point was squashed away. It was a personal project's running commentary and carried a name and a couple of machine paths that have no business in a public repository; the tree is what mattered and the tree is here.
67 lines
2.7 KiB
Kotlin
67 lines
2.7 KiB
Kotlin
package com.example.aiapp
|
|
|
|
import android.content.Context
|
|
import java.io.File
|
|
import java.io.PrintWriter
|
|
import java.io.StringWriter
|
|
import java.text.SimpleDateFormat
|
|
import java.util.Date
|
|
import java.util.Locale
|
|
|
|
/**
|
|
* The last crash, kept so the debug button can hand it over.
|
|
*
|
|
* The alternative is asking somebody to reproduce a crash with the phone plugged into a computer
|
|
* and `logcat` running, which is the one thing nobody has set up at the moment it happens -- and a
|
|
* crash report that arrives a day later, without the stack, is a guess. This costs one file write
|
|
* on a process that is already dying, and it turns "it crashes when I open that chat" into the
|
|
* frame it crashed in.
|
|
*
|
|
* Kept until it is read rather than cleared on the next launch: the app restarts before anybody can
|
|
* ask about it, so a log that lives for one session is a log that is never read.
|
|
*/
|
|
private const val CRASH_FILE = "last-crash.txt"
|
|
|
|
/**
|
|
* How much of a stack is kept.
|
|
*
|
|
* This is pasted into a conversation, so it has a budget like any other output written for a
|
|
* reader. The top of a stack is what identifies a crash and the bottom is framework plumbing, so
|
|
* what gets cut is the part nobody reads.
|
|
*/
|
|
private const val CRASH_LIMIT = 4000
|
|
|
|
/**
|
|
* Records uncaught exceptions, then lets the platform do what it was going to do.
|
|
*
|
|
* Chained rather than replacing: the default handler is what shows the "app has stopped" dialog and
|
|
* ends the process, and an app that swallows that instead sits there in an unknown state. This only
|
|
* adds a witness.
|
|
*/
|
|
fun installCrashLog(context: Context) {
|
|
val app = context.applicationContext
|
|
val previous = Thread.getDefaultUncaughtExceptionHandler()
|
|
Thread.setDefaultUncaughtExceptionHandler { thread, error ->
|
|
runCatching { File(app.filesDir, CRASH_FILE).writeText(describe(thread, error)) }
|
|
previous?.uncaughtException(thread, error)
|
|
}
|
|
}
|
|
|
|
private fun describe(thread: Thread, error: Throwable): String {
|
|
val when_ = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date())
|
|
val stack = StringWriter().also { error.printStackTrace(PrintWriter(it)) }.toString()
|
|
val kept =
|
|
if (stack.length <= CRASH_LIMIT) stack
|
|
else stack.take(CRASH_LIMIT) + "\n ... ${stack.length - CRASH_LIMIT} more characters"
|
|
return "$when_ on thread ${thread.name}\n$kept"
|
|
}
|
|
|
|
/** The last crash, or null if there has not been one since it was last read. */
|
|
fun lastCrash(context: Context): String? =
|
|
File(context.applicationContext.filesDir, CRASH_FILE).takeIf { it.exists() }?.readText()
|
|
|
|
/** Forgets the last crash, once somebody has taken a copy of it. */
|
|
fun clearCrash(context: Context) {
|
|
File(context.applicationContext.filesDir, CRASH_FILE).delete()
|
|
}
|