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.
- package/docs/adr/0024-count-public-transcript-messages.md +31 -0
- package/docs/adr/0025-count-assistant-tool-use-messages.md +29 -0
- package/docs/data-model.md +1 -1
- package/docs/interfaces.md +4 -2
- package/package.json +1 -1
- package/src/active-session-registry.ts +17 -3
- package/src/session-transcript.ts +4 -0
- package/tests/active-session-registry.test.ts +29 -0
- package/tests/session-transcript.test.ts +10 -1
|
@@ -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.
|
package/docs/data-model.md
CHANGED
|
@@ -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
|
|
package/docs/interfaces.md
CHANGED
|
@@ -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,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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.
|
|
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
|
});
|