pi-remote-control 1.0.0 → 1.0.1

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.
@@ -0,0 +1,31 @@
1
+ # Count visible conversation messages
2
+
3
+ ## Title
4
+
5
+ Count visible conversation messages
6
+
7
+ ## Status
8
+
9
+ Accepted
10
+
11
+ ## Context
12
+
13
+ The iOS app displays the `messageCount` field returned by project-session and session-state APIs as a user-facing conversation message count. The current daemon value can come from the TUI registration payload, where it is based on Pi session entries. Pi session entries include internal/tool/system records that are not one-to-one with the public transcript messages shown in the app conversation page.
14
+
15
+ This causes the session list count to be noticeably larger than the message count users infer from the conversation page. The iOS app does not use `messageCount` for pagination, cursors, synchronization, or raw Pi-entry accounting.
16
+
17
+ ## Decision
18
+
19
+ Change the daemon-owned public meaning of `messageCount` to the count of visible conversation messages derived from the session's Pi JSONL `sessionFile`.
20
+
21
+ A visible conversation message is a top-level public transcript message with role `user` or `assistant`. Assistant thinking and tool-use blocks are content within an assistant message and do not increment the count separately. Top-level `toolResult`, `system`, internal tool execution, lifecycle, and other non-conversation records are excluded.
22
+
23
+ For active TUI sessions, the daemon treats `messageCount` in TUI registration as an initial hint only. Public session summaries and session-state responses use the daemon-computed visible conversation count when the session file is readable.
24
+
25
+ ## Consequences
26
+
27
+ The session list count matches the app's conversation-page message model instead of raw Pi session-entry counts or tool/system activity counts.
28
+
29
+ Changing `messageCount` semantics is acceptable because the app only uses it for display. Clients that need raw Pi entry counts should use a separate future field rather than overloading `messageCount`.
30
+
31
+ Computing the count may require reading the session file when building session summaries. Active remote-control session counts are expected to be small enough for this to be acceptable; if needed, the daemon can cache the computed count while preserving the public semantics.
@@ -0,0 +1,29 @@
1
+ # Count assistant tool-use messages
2
+
3
+ ## Title
4
+
5
+ Count assistant tool-use messages
6
+
7
+ ## Status
8
+
9
+ Accepted
10
+
11
+ ## Context
12
+
13
+ ADR 0024 narrowed `messageCount` from raw Pi session-entry count toward the app's conversation display. A real session showed `messageCount` of 79, composed of 6 user messages plus 73 assistant messages. Of those assistant messages, 67 had `stopReason: "toolUse"` and represented assistant tool-call steps; excluding them would produce 12 messages.
14
+
15
+ The iOS app also shows tool-call activity and can display tool-call details, so counting assistant tool-use messages in the session-list count is acceptable.
16
+
17
+ ## Decision
18
+
19
+ Keep `messageCount` as the count of top-level public transcript messages whose role is `user` or `assistant`, including assistant messages whose `stopReason` is `"toolUse"`.
20
+
21
+ Do not count top-level `toolResult`, `system`, tool execution, lifecycle, or other non-message records. Tool-call details remain associated with assistant/tool events in the transcript UI.
22
+
23
+ This ADR supersedes ADR 0024 only for whether assistant tool-use messages are included.
24
+
25
+ ## Consequences
26
+
27
+ The session-list count can be larger than a manually counted user/final-answer exchange count because assistant tool-use steps are included.
28
+
29
+ The count remains useful as an activity-oriented transcript message count and aligns with the app's ability to display tool-call details.
@@ -140,7 +140,7 @@ type ActiveTuiSession = {
140
140
  };
141
141
  ```
142
142
 
143
- An active TUI session is owned by one Pi extension control channel. It is removed when `/remote-control` disables it, the TUI session shuts down, or the control channel closes. If the daemon removes it because heartbeats stopped but the same TUI process still has local remote-control state active, the TUI extension can recreate the active session by re-registering on the next heartbeat miss. Its `sessionFile` points to the Pi JSONL transcript used for HTTP transcript reads. Its `runtimeStatus` is the latest structured runtime-status snapshot reported by the owning TUI extension.
143
+ An active TUI session is owned by one Pi extension control channel. It is removed when `/remote-control` disables it, the TUI session shuts down, or the control channel closes. If the daemon removes it because heartbeats stopped but the same TUI process still has local remote-control state active, the TUI extension can recreate the active session by re-registering on the next heartbeat miss. Its `sessionFile` points to the Pi JSONL transcript used for HTTP transcript reads. Its `runtimeStatus` is the latest structured runtime-status snapshot reported by the owning TUI extension. Public `messageCount` values are daemon-computed counts of top-level `user` and `assistant` transcript messages. Assistant messages whose `stopReason` is `"toolUse"` are included because they represent visible tool-call activity in the app. Thinking and tool-use blocks count as part of their containing assistant message, not as separate messages; top-level `toolResult`, `system`, tool execution, lifecycle, and other non-message records are excluded.
144
144
 
145
145
  ## Runtime status
146
146
 
@@ -89,11 +89,13 @@ Response:
89
89
  }
90
90
  ```
91
91
 
92
+ `messageCount` is the daemon-computed count of top-level `user` and `assistant` transcript messages derived from the session file. Assistant messages whose `stopReason` is `"toolUse"` are included, because the app can show tool-call activity and details. Assistant thinking and tool-use blocks are content within an assistant message and do not increment the count separately. Top-level `toolResult`, `system`, internal tool execution, lifecycle, and other non-message records are excluded.
93
+
92
94
  `POST /v1/projects/{projectId}/sessions` returns `405 method_not_allowed`. New sessions are created in the Pi TUI, then made visible by running `/remote-control`.
93
95
 
94
96
  `GET /v1/sessions/{sessionId}?messageLimit={limit}`
95
97
 
96
- Returns the daemon's current state for an active remote-control TUI session with a bounded recent transcript window read from the session's Pi JSONL `sessionFile`. If `messageLimit` is absent, the daemon uses its default recent-message limit. The daemon enforces a maximum page size. Invalid non-positive limits return `400` with `invalid_limit`.
98
+ Returns the daemon's current state for an active remote-control TUI session with a bounded recent transcript window read from the session's Pi JSONL `sessionFile`. `session.messageCount` uses the transcript message count semantics described above. If `messageLimit` is absent, the daemon uses its default recent-message limit. The daemon enforces a maximum page size. Invalid non-positive limits return `400` with `invalid_limit`.
97
99
 
98
100
  Response:
99
101
 
@@ -406,7 +408,7 @@ Response payload:
406
408
 
407
409
  ### Session registration
408
410
 
409
- When `/remote-control` enables a session, the extension registers the current TUI session:
411
+ When `/remote-control` enables a session, the extension registers the current TUI session. The registration `messageCount` is an initial hint from the TUI; public HTTP responses use daemon-computed transcript message counts from `sessionFile` when available:
410
412
 
411
413
  ```json
412
414
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-remote-control",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "description": "Authenticated remote control for Pi sessions.",
6
6
  "keywords": ["pi-package"],
@@ -1,4 +1,5 @@
1
- import { readSessionTranscriptMessages } from "./session-transcript.js";
1
+ import { existsSync } from "node:fs";
2
+ import { readSessionTranscriptMessages, visibleConversationMessageCount } from "./session-transcript.js";
2
3
  import { DEFAULT_TRANSCRIPT_PAGE_LIMIT, olderTranscriptPage, recentTranscriptWindow, type TranscriptPage } from "./transcript-pagination.js";
3
4
  import type { RuntimeStatus, ToolCallStatus } from "./types.js";
4
5
 
@@ -133,7 +134,7 @@ export function createActiveSessionRegistry(options: ActiveSessionRegistryOption
133
134
  pruneInactiveSessions();
134
135
  return [...sessions.values()]
135
136
  .filter((session) => session.project.id === projectId)
136
- .map((session) => session.summary)
137
+ .map((session) => currentSummary(session))
137
138
  .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
138
139
  },
139
140
 
@@ -145,7 +146,7 @@ export function createActiveSessionRegistry(options: ActiveSessionRegistryOption
145
146
  return {
146
147
  session: {
147
148
  ...session.summary,
148
- messageCount: messages.length,
149
+ messageCount: sessionFileExists(session) ? visibleConversationMessageCount(messages) : session.summary.messageCount,
149
150
  updatedAt: messages.at(-1)?.createdAt ?? session.summary.updatedAt,
150
151
  },
151
152
  ...recentTranscriptWindow(messages, options?.messageLimit ?? DEFAULT_TRANSCRIPT_PAGE_LIMIT),
@@ -191,6 +192,19 @@ export function createActiveSessionRegistry(options: ActiveSessionRegistryOption
191
192
  };
192
193
  }
193
194
 
195
+ function currentSummary(session: StoredActiveSession): ActiveSessionSummary {
196
+ const messages = readSessionTranscriptMessages(session.sessionFile);
197
+ return {
198
+ ...session.summary,
199
+ messageCount: sessionFileExists(session) ? visibleConversationMessageCount(messages) : session.summary.messageCount,
200
+ updatedAt: messages.at(-1)?.createdAt ?? session.summary.updatedAt,
201
+ };
202
+ }
203
+
204
+ function sessionFileExists(session: Pick<ActiveSessionRegistration, "sessionFile">): boolean {
205
+ return session.sessionFile.length > 0 && existsSync(session.sessionFile);
206
+ }
207
+
194
208
  function toSummary(session: ActiveSessionRegistration): ActiveSessionSummary {
195
209
  return {
196
210
  id: session.id,
@@ -2,6 +2,10 @@ import { readFileSync } from "node:fs";
2
2
  import { asRecord, readString, transcriptMessageFromPiMessage } from "./transcript-message.js";
3
3
  import type { TranscriptMessage } from "./types.js";
4
4
 
5
+ export function visibleConversationMessageCount(messages: TranscriptMessage[]): number {
6
+ return messages.filter((message) => message.role === "user" || message.role === "assistant").length;
7
+ }
8
+
5
9
  export function readSessionTranscriptMessages(sessionFile: string): TranscriptMessage[] {
6
10
  let text: string;
7
11
  try {
@@ -108,6 +108,35 @@ describe("active TUI session registry", () => {
108
108
  expect(registry.takeCommands("sess_1")).toEqual([]);
109
109
  });
110
110
 
111
+ it("uses visible conversation message counts for active session summaries", async () => {
112
+ const root = await mkdtemp(join(tmpdir(), "pi-remote-control-registry-count-"));
113
+ const sessionFile = join(root, "session.jsonl");
114
+ const registry = createActiveSessionRegistry();
115
+ try {
116
+ await writeFile(sessionFile, [
117
+ JSON.stringify({ type: "message", id: "msg_1", timestamp: "2026-05-09T00:00:00.000Z", message: { role: "user", content: "hello" } }),
118
+ JSON.stringify({ type: "message", id: "msg_2", timestamp: "2026-05-09T00:00:01.000Z", message: { role: "assistant", content: [{ type: "thinking", thinking: "checking" }, { type: "toolCall", id: "call_1", name: "bash", arguments: { command: "ls" } }, { type: "text", text: "done" }] } }),
119
+ JSON.stringify({ type: "message", id: "msg_3", timestamp: "2026-05-09T00:00:02.000Z", message: { role: "toolResult", toolCallId: "call_1", toolName: "bash", content: [{ type: "text", text: "file.txt" }] } }),
120
+ JSON.stringify({ type: "message", id: "msg_4", timestamp: "2026-05-09T00:00:03.000Z", message: { role: "system", content: "system" } }),
121
+ ].join("\n"));
122
+ registry.registerSession({
123
+ id: "sess_1",
124
+ piSessionId: "pi_1",
125
+ project: { id: "proj_1", name: "Example", path: "/repo/example" },
126
+ sessionFile,
127
+ pid: 1234,
128
+ messageCount: 999,
129
+ isStreaming: false,
130
+ updatedAt: "2026-05-09T00:00:00.000Z",
131
+ });
132
+
133
+ expect(registry.listProjectSessions("proj_1")[0]?.messageCount).toBe(2);
134
+ expect(registry.getSessionState("sess_1")?.session.messageCount).toBe(2);
135
+ } finally {
136
+ await rm(root, { recursive: true, force: true });
137
+ }
138
+ });
139
+
111
140
  it("returns snapshots for active sessions", async () => {
112
141
  const root = await mkdtemp(join(tmpdir(), "pi-remote-control-registry-"));
113
142
  const sessionFile = join(root, "session.jsonl");
@@ -2,7 +2,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { tmpdir } from "node:os";
4
4
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- import { readSessionTranscriptMessages } from "../src/session-transcript.js";
5
+ import { readSessionTranscriptMessages, visibleConversationMessageCount } from "../src/session-transcript.js";
6
6
 
7
7
  let root: string;
8
8
 
@@ -69,6 +69,15 @@ describe("session transcript files", () => {
69
69
  ]);
70
70
  });
71
71
 
72
+ it("counts only visible user and assistant conversation messages", () => {
73
+ expect(visibleConversationMessageCount([
74
+ { id: "msg_1", role: "user", content: [{ type: "text", text: "hello" }], text: "hello", createdAt: "2026-05-09T00:00:01.000Z", isStreaming: false },
75
+ { id: "msg_2", role: "assistant", content: [{ type: "thinking", thinking: "checking" }, { type: "toolCall", id: "call_1", name: "bash", arguments: { command: "ls" } }, { type: "text", text: "done" }], text: "done", createdAt: "2026-05-09T00:00:02.000Z", isStreaming: false },
76
+ { id: "msg_3", role: "toolResult", content: [{ type: "text", text: "file.txt" }], text: "file.txt", createdAt: "2026-05-09T00:00:03.000Z", isStreaming: false },
77
+ { id: "msg_4", role: "system", content: [{ type: "text", text: "system" }], text: "system", createdAt: "2026-05-09T00:00:04.000Z", isStreaming: false },
78
+ ])).toBe(2);
79
+ });
80
+
72
81
  it("returns an empty transcript when the session file is missing", () => {
73
82
  expect(readSessionTranscriptMessages(join(root, "missing.jsonl"))).toEqual([]);
74
83
  });