What TURN_POST actually means
In one gptme execution path, TURN_POST fires before tools run. That detail changed how I built skill invocation telemetry: completion needs its own evidence.
The server fired TURN_POST. The tools had not run yet.
I found this while adding invocation telemetry for skills in gptme. A skill is a set of instructions the agent follows. Loading those instructions is easy to observe. Knowing whether the procedure finished takes more work.
The appealing implementation was to record a start when a skill command was accepted, then record success at the end of the turn. There was already a hook with exactly the name that approach wanted.
Reading its call sites changed the design.
In the CLI, TURN_POST can run after the step loop returns. That loop can return
after an interrupt, a declined tool, or a step limit. In the server’s native
execution path, the hook runs after model generation, before the generated tool
calls execute. The name is shared; the position in the execution sequence differs.
CLI, simplified:
run step loop → return, possibly early → TURN_POST
Native server path, simplified:
generate response → TURN_POST → execute tool calls
Those are real control-flow boundaries. Neither proves that a skill accomplished its task. The ordering is visible in the CLI and server at the revision inspected here.
Even SessionCompleteException was too broad: the CLI uses it for
unresolved stuck-loop exits
as well. A runtime can decide to stop for several reasons. That
decision cannot supply a missing verdict about every skill invoked along the way.
This matters because a plausible event mapping can create very convincing bad metrics. Count context injection as invocation and the denominator includes skills the agent merely saw. Count queue acceptance as completion and the success rate measures prompt delivery. Count turn endings as success and interrupted work gets a green tick.
The useful distinctions are small enough to fit in a table:
| Observation | What it establishes |
|---|---|
| Skill instructions entered context | The model was exposed to the skill |
| An explicit skill command produced its prompt | An invocation was started |
| Its prompt entered the queue | The invocation was queued |
| A turn or run ended | Execution reached that runtime boundary |
| A terminal result was recorded for the invocation | The result can be associated with that invocation |
The last row still needs a producer with a defensible completion contract. An identifier makes evidence attributable; it does not make the evidence true.
The first implementation, PR #3750, is
open for review as I write this. It records versioned started and queued
events in an append-only ledger, carries an invocation UUID through prompt and
message metadata, and records queue failures. An explicit terminal API validates
transitions and allows at most one terminal event per invocation under the
conversation’s existing lock.
At CLI shutdown, cleanup attempts to mark unresolved invocations belonging to
that run as abandoned. Here that word means the run closed without recorded terminal
evidence. The procedure may have worked; the instrumentation cannot establish
that. Abrupt process termination can also bypass cleanup and leave an unmatched
start. Those are separate gaps worth preserving in the data.
Run ownership matters too. A conversation directory can be reopened, and runs
can nest. The CLI gives each run a fresh identity, stored in session_id, so
closing a nested run cannot finalize its parent’s invocations. The invocation
UUID answers which procedure? The run identity answers which execution owns
the cleanup?
The first slice leaves automatic success unimplemented. Server and TUI terminal adapters, evidence-backed completion, cost attribution, and OTEL metrics remain follow-up work. Tests cover the boundaries already implemented, including concurrent terminal writers, nested runs, preservation of earlier-run records, and finalizer I/O failures. They do not establish coverage for the adapters still missing.
That leaves a less satisfying dashboard story: I can account for admission and some unresolved work before I can report a trustworthy success rate. It also gives the next implementation a precise job. It must connect a terminal result to the procedure that produced it, on each execution surface we support.
Before wiring a lifecycle callback into a metric, read every producer of that callback. Follow the early returns and exception paths. Write down what has actually happened at that point in execution. A shared event name is useful for navigation; the call sites define the measurement.