@taylorsatula/pi-timestamp 0.1.0 → 0.2.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/README.md CHANGED
@@ -4,31 +4,37 @@ Timestamp injection extension for [Pi](https://pi.dev) — the model always know
4
4
 
5
5
  ## What it does
6
6
 
7
- Hooks `before_provider_request` and appends a synthetic tool-call pair to the outgoing message list, carrying the current local wall-clock time:
8
-
9
- ```jsonc
10
- [
11
- {
12
- "role": "assistant",
13
- "tool_calls": [
14
- {
15
- "id": "ts_1756220400000",
16
- "type": "function",
17
- "function": { "name": "__timestamp", "arguments": "{\"timestamp\":\"[3:00pm - 8/26/25]\"}" }
18
- }
19
- ]
20
- },
21
- { "role": "tool", "tool_call_id": "ts_1756220400000", "content": "ok" }
22
- ]
23
- ```
24
-
25
- Injection happens only at **turn boundaries**, not on every provider call:
7
+ Hooks `before_provider_request` and appends the current local wall-clock time as plain text
8
+ to the tail of the last message:
26
9
 
27
- - Last message is a **user message** → inject.
28
- - Last message is an **assistant message with text content** (a vocalization between tool steps) → inject.
29
- - Last message is an assistant message with only `tool_calls`, or a tool result (mid-turn churn) → skip.
10
+ ```
11
+ They're injected poorly if they're that annoying.
12
+ [11:34pm - 9/1/26]
13
+ ```
30
14
 
31
- This keeps the timestamp fresh each time control returns to the user-facing side of the loop, without spamming intermediate tool-loop requests.
15
+ Injection happens only at **turn boundaries**, never mid-tool-loop:
16
+
17
+ - Last message is a **user message** → append the timestamp to its content.
18
+ - Last message is an **assistant message with text content** (a vocalization between tool
19
+ steps) → append to its content.
20
+ - Last message is an assistant message carrying `tool_calls`, or a tool result (mid-turn
21
+ churn) → skip.
22
+
23
+ ## Why plain-text tail append (design notes)
24
+
25
+ - **Cache safety.** The tail of the message list is the only cache-safe injection point:
26
+ mutating the system prompt would invalidate the KV-cache prefix on every request; a
27
+ tail mutation re-processes only the last message. The system prompt is never touched.
28
+ - **No synthetic tool calls.** Appending after an assistant message that carries a
29
+ `tool_call` would require fabricating a tool-call pair (a call must be answered by its
30
+ result) — so those positions are simply never injected. In the positions that are
31
+ injected, no disguise is needed, so none is used. The model never appears to call a
32
+ nonexistent tool, and transcripts never render phantom calls.
33
+ - **Idempotent.** A previously appended tag is stripped before the fresh one is written,
34
+ so re-fires and retries cannot stack timestamps.
35
+ - **Content-shape aware.** String content concatenates directly; block-array content
36
+ (Anthropic-shaped) appends into or after the last text block; unknown shapes are left
37
+ untouched rather than guessed at.
32
38
 
33
39
  ## Installation
34
40
 
@@ -46,9 +52,15 @@ Or add to `~/.pi/agent/settings.json` under `packages`:
46
52
 
47
53
  ## Notes
48
54
 
49
- - Timestamps use the machine's **local time**, formatted `h:mmam/pm - M/D/YY` (e.g. `[3:07pm - 8/26/25]`).
50
- - No tools, commands, shortcuts, or settings are registered — the extension is invisible except for the injected messages.
51
- - The injected pair is added to the outgoing payload only; it is not persisted to the session transcript.
55
+ - Timestamps use the machine's **local time**, formatted `h:mmam/pm - M/D/YY`
56
+ (e.g. `[3:07pm - 8/26/25]`).
57
+ - No tools, commands, shortcuts, or settings are registered — the extension is invisible
58
+ except for the appended tag.
59
+ - The tag is added to the outgoing payload only; it is not persisted to the session
60
+ transcript.
61
+ - Version 0.2.0 changed the mechanism (plain-text tail append replacing the synthetic
62
+ tool-call pair). Sessions started before the upgrade may still show one old-style pair
63
+ until restart.
52
64
 
53
65
  ## Development
54
66
 
@@ -1,5 +1,22 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
+ // Cache-correct tail injection: mutating the system prompt would invalidate the
4
+ // KV-cache prefix on every request; the last message is the only cache-safe
5
+ // injection point, and only when no tool call is pending.
6
+ //
7
+ // Design:
8
+ // - Inject into user messages and text-only assistant vocalizations by
9
+ // appending plain text to the message content. No fabricated tool-call
10
+ // pair — the pair disguise is only structurally required in tool-call
11
+ // positions, which we never inject into.
12
+ // - Never inject after an assistant message carrying tool_calls (appending
13
+ // there would require wedging a pair between the call and its result) and
14
+ // never after tool results (mid-loop churn).
15
+ // - Idempotent per request: a previously appended tag is stripped before the
16
+ // fresh one is written.
17
+
18
+ const TS_STRIP = /\n\[\d{1,2}:\d{2}(am|pm) - \d{1,2}\/\d{1,2}\/\d{2}\]\s*$/;
19
+
3
20
  function formatTimestamp(): string {
4
21
  const now = new Date();
5
22
  let hours = now.getHours();
@@ -13,52 +30,46 @@ function formatTimestamp(): string {
13
30
  return `[${hours}:${String(minutes).padStart(2, "0")}${ampm} - ${month}/${day}/${year}]`;
14
31
  }
15
32
 
16
- function shouldInject(msgs: Array<Record<string, unknown>>): boolean {
33
+ function injectable(msgs: Array<Record<string, unknown>>): boolean {
17
34
  const last = msgs[msgs.length - 1];
18
35
  if (!last) return false;
19
36
 
20
- // User message → inject
21
37
  if (last.role === "user") return true;
22
38
 
23
- // Assistant with text content (vocalization) → inject
24
- // Assistant with only tool_calls (intermediate) → skip
25
39
  if (last.role === "assistant") {
26
40
  const hasToolCalls = Array.isArray(last.tool_calls) && last.tool_calls.length > 0;
27
41
  const hasContent = last.content != null && last.content !== "";
28
- return !hasToolCalls && hasContent;
42
+ return !hasToolCalls && hasContent; // vocalization → inject; tool call → never
29
43
  }
30
44
 
31
- // Tool results → skip (intermediate)
32
- return false;
45
+ return false; // tool results → mid-loop, skip
33
46
  }
34
47
 
35
48
  export default function (pi: ExtensionAPI) {
36
49
  pi.on("before_provider_request", (event) => {
37
50
  const payload = event.payload as Record<string, unknown>;
38
51
  const msgs = payload.messages as Array<Record<string, unknown>>;
52
+ if (!injectable(msgs)) return;
39
53
 
40
- if (!shouldInject(msgs)) return;
41
-
42
- const timestamp = formatTimestamp();
43
- const callId = `ts_${Date.now()}`;
54
+ const ts = `\n${formatTimestamp()}`;
55
+ const last = msgs[msgs.length - 1];
44
56
 
45
- msgs.push(
46
- {
47
- role: "assistant",
48
- tool_calls: [
49
- {
50
- id: callId,
51
- type: "function",
52
- function: { name: "__timestamp", arguments: JSON.stringify({ timestamp }) },
53
- },
54
- ],
55
- },
56
- {
57
- role: "tool",
58
- tool_call_id: callId,
59
- content: "ok",
60
- },
61
- );
57
+ if (typeof last.content === "string") {
58
+ last.content = last.content.replace(TS_STRIP, "") + ts;
59
+ } else if (Array.isArray(last.content)) {
60
+ // Provider-shaped block arrays (e.g. Anthropic): normalize the last
61
+ // text block in place, or append a text block when none exists.
62
+ const blocks = [...(last.content as Array<Record<string, unknown>>)];
63
+ const idx = blocks.length - 1;
64
+ const lastBlock = blocks[idx] as { type?: string; text?: string } | undefined;
65
+ if (lastBlock && lastBlock.type === "text" && typeof lastBlock.text === "string") {
66
+ blocks[idx] = { ...lastBlock, text: lastBlock.text.replace(TS_STRIP, "") + ts };
67
+ } else {
68
+ blocks.push({ type: "text", text: ts });
69
+ }
70
+ last.content = blocks;
71
+ }
72
+ // Non-string, non-array content shapes: leave untouched rather than guess.
62
73
 
63
74
  return event.payload;
64
75
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@taylorsatula/pi-timestamp",
3
- "version": "0.1.0",
4
- "description": "Timestamp injection extension for Pi — appends the current wall-clock time as a synthetic tool-call pair to every provider request at turn boundaries, so the model always knows what time it is.",
3
+ "version": "0.2.0",
4
+ "description": "Timestamp injection extension for Pi — appends the current wall-clock time to the tail of user messages and vocalizations just-in-time (cache-safe, no synthetic tool calls), so the model always knows what time it is.",
5
5
  "keywords": [
6
6
  "pi-package"
7
7
  ],