pi-quiver 6.7.1 → 6.8.0

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/CHANGELOG.md CHANGED
@@ -8,6 +8,10 @@ Published to npm as `pi-quiver` (`pi install npm:pi-quiver`). Pushing a
8
8
  via OIDC trusted publishing. The release helper at
9
9
  `.agents/skills/release/scripts/release.sh` cuts the tag; CI publishes.
10
10
 
11
+ ## v6.8.0 - 2026-10-01
12
+
13
+ - `session-name`: opt-in automatic naming starts in the background after three completed model/tool rounds. Shorter runs start a non-blocking attempt at run end; initial generation is best-effort with a 30-second local deadline and no automatic retry per session activation.
14
+
11
15
  ## v6.7.1 - 2026-10-01
12
16
 
13
17
  - `slack_post`/`slack_update` explain native Slack representations for skill-required formatting, with complete readable fallback; otherwise plain content stays plain.
package/README.md CHANGED
@@ -199,7 +199,7 @@ same outcome here because `enabled` is the only field either layer sets, but
199
199
  the merge is per-field: a global field with no project counterpart would
200
200
  survive untouched.
201
201
 
202
- `sessionAutoName.enabled` makes one extra short LLM call per session (once, after the first turn) to title it; `false` (default) makes no model calls. `rules` appends house conventions to the naming prompt (later rules win when they conflict with the built-ins). Literal, case-insensitive `deny` phrases are stripped from every name; whitespace inside a phrase is loose, so `"acme corp"` also catches `AcmeCorp`. `revisitFirstTurn` re-evaluates the name once that many model round trips have completed, while `revisitEveryTurns` does so at every multiple; both default to `0` (off) because each revisit costs another short LLM call. For example, `10` and `100` mark round trips 10, 100, 200, 300. Revisits only run when the agent has fully settled (idle, nothing queued) - an automated multi-turn run such as a subagent chain is never renamed or delayed mid-flight; cadence points it crossed fire once, at the settle. A machine-generated name is replaced when stale. A name set by a human is never overwritten: the extension strongly prefers it, and announces a suggestion only when the work has clearly moved on. Counts come from the persisted transcript, so they survive resume.
202
+ `sessionAutoName.enabled` starts one background initial naming attempt for an unnamed session after three completed model/tool rounds in the current agent run, not three user messages. A round is one completed assistant turn and its tool results, regardless of how many tools run. If the run ends sooner, it starts the same background attempt at run end. Initial naming is best-effort: preparation and generation have a 30-second local deadline, and the agent, editor return, and shutdown do not wait for naming. The session can remain unnamed if generation fails, times out, or the process exits first; there is no automatic retry during that session activation, though reopening an unnamed session allows another attempt on its next run. An existing name suppresses initial naming, and a human rename while generation is pending takes precedence. `false` (default) makes no automatic naming model calls. `rules` appends house conventions to the naming prompt (later rules win when they conflict with the built-ins). Literal, case-insensitive `deny` phrases are stripped from every name; whitespace inside a phrase is loose, so `"acme corp"` also catches `AcmeCorp`. `revisitFirstTurn` re-evaluates the name once that many model round trips have completed, while `revisitEveryTurns` does so at every multiple; both default to `0` (off) because each revisit costs another short LLM call. For example, `10` and `100` mark round trips 10, 100, 200, 300. Revisits only run when the agent has fully settled (idle, nothing queued) - an automated multi-turn run such as a subagent chain is never renamed or delayed mid-flight by a revisit; cadence points it crossed fire once, at the settle. A machine-generated name is replaced when stale. A name set by a human is never overwritten: the extension strongly prefers it, and announces a suggestion only when the work has clearly moved on. Revisit counts come from the persisted transcript, so they survive resume.
203
203
 
204
204
  `herdrTab` (default `true`) mirrors the same curated label to the Herdr tab bar over Herdr's unix socket, independent of `ghosttyTab` - either sink can be toggled off without affecting the other. A tab is claimable while its label is a bare number (`1`, `4`, ...) at any position - Herdr's vocabulary for "unnamed" - so reordering tabs before or after naming never matters; a tab you deliberately name `3` is taken over too. A non-numeric label is treated as a human's and is never overwritten; the check repeats every turn, so renumbering the tab by hand hands it back. Exactly one leading `* ` (herdr-ntfy-notify's armed marker) is not a human rename - it is preserved across renames and the shutdown restore, and removing it keeps the claim. Every session claims afresh: on each shutdown (quit, reload, `/new`, resume, fork) a claimed tab is restored to its then-current position number, and the successor session in the pane claims it again on its first name. The label the extension writes is never digits-only - a bare `1234` becomes `#1234`, and the naming prompt asks the model for `PR 1234` / `issue 123` / `ticket ABC-123` instead of a bare ID. It only ever runs in TUI mode, on an attached TTY, under Herdr (`HERDR_ENV`/`HERDR_TAB_ID`/`HERDR_SOCKET_PATH` all set) - `pi -p`/json/rpc runs and background subagents never touch the tab. Caveat: the restored number is internally a custom name (Herdr has no clear-to-auto API), so that tab keeps a number-looking label but stops renumbering on later tab closes/reorders.
205
205
 
@@ -2,7 +2,7 @@
2
2
  * Session naming.
3
3
  *
4
4
  * - /session-name [name] : manually set or show the session name.
5
- * - Auto-naming : after the first agent turn, derive a concise name
5
+ * - Auto-naming : after three rounds (or a shorter run), derive a name
6
6
  * from the conversation (unless one is already set).
7
7
  * - Revisiting : re-derive the name later in a long session, once
8
8
  * the work has revealed what it actually is.
@@ -372,16 +372,21 @@ export function withAuthBaseUrl<M extends { baseUrl: string }>(
372
372
  return auth.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model;
373
373
  }
374
374
 
375
- async function generateName(
375
+ export async function generateName(
376
376
  ctx: ExtensionContext,
377
377
  opts: PromptOptions = {},
378
+ signal?: AbortSignal,
379
+ getComplete: () => Promise<CompleteFn> = loadComplete,
378
380
  ): Promise<typeof KEEP | GeneratedName | undefined> {
381
+ if (signal?.aborted) return undefined;
379
382
  const conversation = buildConversationText(ctx, 4000, Boolean(opts.currentName));
380
383
  if (conversation.length < 8) return undefined;
381
384
 
382
385
  const model = ctx.model;
383
386
  if (!model) return undefined;
387
+ const prompt = buildNamingPrompt(conversation, opts);
384
388
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
389
+ if (signal?.aborted) return undefined;
385
390
  // ok=true with apiKey=undefined is the env-key path: the key lives in
386
391
  // process.env (e.g. ANTHROPIC_API_KEY), not auth.json. getApiKeyAndHeaders
387
392
  // deliberately opts out of the env fallback (includeFallback: false), so it
@@ -389,9 +394,8 @@ async function generateName(
389
394
  // itself via withEnvApiKey/getEnvApiKey. Only bail when auth genuinely failed.
390
395
  if (!auth?.ok) return undefined;
391
396
 
392
- const prompt = buildNamingPrompt(conversation, opts);
393
-
394
- const complete = await loadComplete();
397
+ const complete = await getComplete();
398
+ if (signal?.aborted) return undefined;
395
399
  const response = await complete(
396
400
  withAuthBaseUrl(model, auth),
397
401
  {
@@ -399,9 +403,10 @@ async function generateName(
399
403
  { role: "user" as const, content: [{ type: "text" as const, text: prompt }], timestamp: Date.now() },
400
404
  ],
401
405
  },
402
- { apiKey: auth.apiKey, headers: auth.headers, env: auth.env, reasoningEffort: "low" },
406
+ { apiKey: auth.apiKey, headers: auth.headers, env: auth.env, reasoningEffort: "low", signal },
403
407
  );
404
408
 
409
+ if (signal?.aborted) return undefined;
405
410
  const raw = response.content
406
411
  .filter((c): c is { type: "text"; text: string } => c.type === "text")
407
412
  .map((c) => c.text)
@@ -413,10 +418,16 @@ async function generateName(
413
418
  type NameGenerator = (
414
419
  ctx: ExtensionContext,
415
420
  opts?: PromptOptions,
421
+ signal?: AbortSignal,
416
422
  ) => Promise<typeof KEEP | GeneratedName | undefined>;
417
423
 
418
424
  export function installSessionName(pi: ExtensionAPI, generate: NameGenerator = generateName) {
419
425
  let autoNameTried = false;
426
+ let activation = 0;
427
+ let initialRounds = 0;
428
+ let initialDone: Promise<void> = Promise.resolve();
429
+ let initialAttempt: { cancel: () => void } | null = null;
430
+ const invalidateInitial = (): void => { initialAttempt?.cancel(); };
420
431
  // Who chose the current name. A human's wording is never overwritten by a
421
432
  // revisit - at most we suggest - so unknown provenance (a resumed session, a
422
433
  // rename from outside this extension) is treated as human.
@@ -472,6 +483,7 @@ export function installSessionName(pi: ExtensionAPI, generate: NameGenerator = g
472
483
  tabLabel?: string,
473
484
  author?: NameAuthor,
474
485
  mode?: Mode,
486
+ ownsName?: () => boolean,
475
487
  ): Promise<void> => {
476
488
  const clean = applyDenyList(name, cfg.deny);
477
489
  if (author) recordNameAuthor(clean, author);
@@ -480,7 +492,7 @@ export function installSessionName(pi: ExtensionAPI, generate: NameGenerator = g
480
492
  lastSyncedName = clean;
481
493
  currentTabLabel = toTabLabel(applyDenyList(tabLabel ?? clean, cfg.deny));
482
494
  renameGhosttyTab(currentTabLabel, cfg.ghosttyTab);
483
- await syncHerdrTab(cfg, currentTabLabel, mode);
495
+ await syncHerdrTab(cfg, currentTabLabel, mode, ownsName);
484
496
  };
485
497
 
486
498
  // Re-assert the tab from the current session name. Self-heals when the name
@@ -533,19 +545,23 @@ export function installSessionName(pi: ExtensionAPI, generate: NameGenerator = g
533
545
  return { claimable: false, armed: false };
534
546
  };
535
547
 
536
- const syncHerdrTab = (cfg: Config, label: string | null, mode: Mode | undefined): Promise<void> => {
548
+ const syncHerdrTab = (cfg: Config, label: string | null, mode: Mode | undefined, ownsName?: () => boolean): Promise<void> => {
537
549
  const run = async (): Promise<void> => {
550
+ if (ownsName && !ownsName()) return;
538
551
  if (!cfg.herdrTab || mode !== "tui" || !label) return;
539
552
  if (!isHerdrActive()) return;
540
553
  const sock = process.env.HERDR_SOCKET_PATH as string;
541
554
  const tabId = process.env.HERDR_TAB_ID as string;
542
555
  const live = await getTab(sock, tabId, HERDR_TIMEOUT_MS);
556
+ if (ownsName && !ownsName()) return;
543
557
  if (!live) return; // transient or stale tab id; a failed read is never a human rename
544
558
  if (typeof herdrClaim === "object" && herdrClaim !== null) {
545
559
  const owned = matchOwned(live.label, herdrClaim.lastWritten);
546
560
  if (owned.owned) {
547
561
  if (label === herdrClaim.lastWritten) return;
548
- if (await renameTab(sock, tabId, (owned.armed ? ARMED_PREFIX : "") + label, HERDR_TIMEOUT_MS)) herdrClaim.lastWritten = label;
562
+ if (await renameTab(sock, tabId, (owned.armed ? ARMED_PREFIX : "") + label, HERDR_TIMEOUT_MS)) {
563
+ herdrClaim.lastWritten = label;
564
+ }
549
565
  return;
550
566
  }
551
567
  }
@@ -589,6 +605,7 @@ export function installSessionName(pi: ExtensionAPI, generate: NameGenerator = g
589
605
  handler: async (args, ctx) => {
590
606
  const name = args.trim();
591
607
  if (name) {
608
+ invalidateInitial();
592
609
  autoNameTried = true; // manual name wins; don't auto-overwrite later
593
610
  await setName(loadConfig(ctx), name, undefined, "human", ctx.mode);
594
611
  ctx.ui.notify(`Session named: ${pi.getSessionName()}`, "info");
@@ -600,6 +617,15 @@ export function installSessionName(pi: ExtensionAPI, generate: NameGenerator = g
600
617
  });
601
618
 
602
619
  pi.on("session_start", async (_event, ctx) => {
620
+ invalidateInitial();
621
+ activation++;
622
+ autoNameTried = false;
623
+ initialRounds = 0;
624
+ nameAuthor = "human";
625
+ lastRevisitAt = 0;
626
+ expectedInternalName = null;
627
+ lastSyncedName = null;
628
+ currentTabLabel = null;
603
629
  // Every session claims afresh, exactly like a fresh process: /new, resume,
604
630
  // and fork each tear the previous session down (restore included) first.
605
631
  // Before loadConfig so `enabled: false, herdrTab: true` resets too.
@@ -636,6 +662,8 @@ export function installSessionName(pi: ExtensionAPI, generate: NameGenerator = g
636
662
  return;
637
663
  }
638
664
  expectedInternalName = null;
665
+ invalidateInitial();
666
+ autoNameTried = true;
639
667
  if (!current) return;
640
668
  const cfg = loadConfig(ctx);
641
669
  if (!cfg.enabled) return;
@@ -659,24 +687,60 @@ export function installSessionName(pi: ExtensionAPI, generate: NameGenerator = g
659
687
  // internally a custom name - Herdr has no clear-to-auto API - so it won't
660
688
  // renumber on reorders; the successor claims any numeric label regardless.
661
689
  pi.on("session_shutdown", async () => {
690
+ invalidateInitial();
691
+ activation++;
662
692
  await restoreHerdrTab();
663
693
  });
664
694
 
665
- pi.on("agent_end", async (_event, ctx) => {
695
+ const startInitial = (ctx: ExtensionContext): void => {
666
696
  if (autoNameTried || pi.getSessionName()) return;
667
697
  const cfg = loadConfig(ctx);
668
- if (!cfg.enabled) return; // off by default; opt in via settings.json
698
+ if (!cfg.enabled) return;
669
699
  autoNameTried = true;
670
- try {
671
- const generated = await generate(ctx, { rules: cfg.rules });
672
- if (generated && generated !== KEEP && !pi.getSessionName()) {
673
- await setName(cfg, generated.sessionName, generated.tabLabel, "auto", ctx.mode);
674
- if (ctx.hasUI) ctx.ui.notify(`Auto-named session: ${pi.getSessionName()}`, "info");
700
+ const identity = activation;
701
+ const mode = ctx.mode;
702
+ const notify = ctx.hasUI ? ctx.ui.notify.bind(ctx.ui) : undefined;
703
+ const controller = new AbortController();
704
+ let active = true;
705
+ let settle!: () => void;
706
+ initialDone = new Promise<void>((resolve) => { settle = resolve; });
707
+ const finish = (): void => {
708
+ if (!active) return;
709
+ active = false;
710
+ clearTimeout(timer);
711
+ if (initialAttempt === attempt) initialAttempt = null;
712
+ settle();
713
+ };
714
+ const attempt = { cancel: (): void => { finish(); controller.abort(); } };
715
+ const timer = setTimeout(attempt.cancel, 30_000);
716
+ timer.unref();
717
+ initialAttempt = attempt;
718
+ void (async () => {
719
+ try {
720
+ const generated = await generate(ctx, { rules: cfg.rules }, controller.signal);
721
+ if (!active || identity !== activation || controller.signal.aborted) return;
722
+ if (!generated || generated === KEEP || pi.getSessionName()) return;
723
+ clearTimeout(timer);
724
+ const applied = applyDenyList(generated.sessionName, cfg.deny);
725
+ await setName(cfg, generated.sessionName, generated.tabLabel, "auto", mode,
726
+ () => active && identity === activation && !controller.signal.aborted && pi.getSessionName() === applied);
727
+ if (active && identity === activation && !controller.signal.aborted && pi.getSessionName() === applied) {
728
+ notify?.(`Auto-named session: ${applied}`, "info");
729
+ }
730
+ } catch {
731
+ // Naming is best-effort, including late failures after cancellation.
732
+ } finally {
733
+ finish();
675
734
  }
676
- } catch {
677
- // best-effort; ignore failures
678
- }
735
+ })();
736
+ };
737
+
738
+ pi.on("agent_start", () => { initialRounds = 0; });
739
+ pi.on("turn_end", (_event, ctx) => {
740
+ initialRounds++;
741
+ if (initialRounds >= 3) startInitial(ctx);
679
742
  });
743
+ pi.on("agent_end", (_event, ctx) => { startInitial(ctx); });
680
744
 
681
745
  // Revisit. A name derived from the first turn describes the opening move,
682
746
  // which is frequently not what the session turns out to be about - the work
@@ -724,7 +788,7 @@ export function installSessionName(pi: ExtensionAPI, generate: NameGenerator = g
724
788
  })();
725
789
  });
726
790
 
727
- return { revisitSettled: () => revisitDone };
791
+ return { initialSettled: () => initialDone, revisitSettled: () => revisitDone };
728
792
  }
729
793
 
730
794
  export default function (pi: ExtensionAPI) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-quiver",
3
- "version": "6.7.1",
3
+ "version": "6.8.0",
4
4
  "description": "Personal pack of Pi coding-agent extensions: context-safe fetch, doc_to_md PDF/DOCX/PPTX-to-Markdown conversion, session naming, a themed ASCII startup header, Opus 4.8 fast mode, and a provider-stall watchdog.",
5
5
  "author": "Jacek Juraszek",
6
6
  "license": "MIT",