privateer-agent 0.12.33 → 0.12.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "privateer-agent",
3
- "version": "0.12.33",
3
+ "version": "0.12.34",
4
4
  "description": "Privacy-first terminal coding agent — bring your own model across 20 providers (Anthropic, OpenAI, OpenRouter, Google, local Ollama…). Safe-by-default permissions, MCP, sub-agents, workflows, and verifiable TEE inference. Built on the Pi toolkit.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli/chat.ts CHANGED
@@ -329,6 +329,15 @@ async function main() {
329
329
  async function runTurn(text: string, remote: boolean, echo = true): Promise<void> {
330
330
  if (turnActive) {
331
331
  console.log(`\n${DIM}(busy — a turn is already running)${RESET}`);
332
+ // Say it to the DRIVER too, not just to this terminal's own console. An
333
+ // app-driven prompt refused here used to vanish: the app had already echoed
334
+ // the message into its feed and lit the thinking pill (submit() in
335
+ // RemoteDriveContext does both before transmitting), and the only trace of
336
+ // the refusal was a dim line on a terminal nobody was looking at. The driver
337
+ // saw their own message sit under a spinner forever. A notice does not close
338
+ // their turn, and must not — a turn IS running, and it is the one they are
339
+ // watching — but it tells them this message was not added to it.
340
+ if (remote) relay?.sendNotice("Not sent — this terminal is already running a turn. Wait for it to finish, or press Stop.");
332
341
  return;
333
342
  }
334
343
  turnActive = true;
@@ -36,7 +36,20 @@ export type EngineEvent =
36
36
  | { type: "retrying"; attempt: number; max: number; delayMs: number; reason: string }
37
37
  // `error` is the short user-facing message; `hint` is an optional actionable
38
38
  // next step rendered dim beneath it. Both are already secret-redacted.
39
- | { type: "error"; error: string; hint?: string; retryable?: boolean };
39
+ | { type: "error"; error: string; hint?: string; retryable?: boolean }
40
+ // The turn is still running and has produced nothing for `sinceMs`. Emitted by a
41
+ // HOST that supervises turns (today: the desktop's turnSupervisor), never by the
42
+ // engine — the engine has no clock and no view of the turn as a whole.
43
+ //
44
+ // It exists because silence is ambiguous and the app cannot resolve it: a tool
45
+ // call emits at its start and its end and nothing in between, so a fifteen-minute
46
+ // build, a stalled model socket and a dead agent all look identical from the far
47
+ // side of the relay. This is the frame that says which. `tool`/`toolMs` name what
48
+ // is being waited on when something is.
49
+ //
50
+ // It is NOT turn activity: it must not reset a "silent for 4m" clock, or it would
51
+ // erase the very thing it was sent to report.
52
+ | { type: "still-working"; sinceMs: number; tool?: string; toolMs?: number };
40
53
 
41
54
  export const emptyUsage = (): UsageTotals => ({
42
55
  inputTokens: 0,
@@ -373,6 +373,11 @@ function projectEvent(ev: EngineEvent): Record<string, unknown> {
373
373
  return { type: "aborted" };
374
374
  case "step-finish":
375
375
  return { type: "step-finish" };
376
+ // Fields, not just the tag: the default below would drop sinceMs/tool and the
377
+ // frame would arrive saying "something is happening" without saying what or
378
+ // for how long, which is the whole of its content.
379
+ case "still-working":
380
+ return { type: "still-working", sinceMs: ev.sinceMs, tool: ev.tool, toolMs: ev.toolMs };
376
381
  default:
377
382
  // text/reasoning are coalesced in sendEvent and never reach here.
378
383
  return { type: ev.type };