@counterposition/skill-pi 0.1.1 → 0.65.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/README.md +3 -0
- package/package.json +1 -1
- package/skills/pi/README.md +24 -20
- package/skills/pi/SKILL.md +59 -25
- package/skills/pi/references/extensions.md +150 -234
- package/skills/pi/references/packages.md +89 -59
- package/skills/pi/references/patterns.md +4 -4
- package/skills/pi/references/providers.md +101 -153
- package/skills/pi/references/sdk.md +174 -118
- package/skills/pi/references/settings.md +119 -100
- package/skills/pi/references/skills.md +83 -73
|
@@ -1,30 +1,42 @@
|
|
|
1
1
|
# Extensions
|
|
2
2
|
|
|
3
|
-
Extensions are TypeScript modules that
|
|
3
|
+
Extensions are TypeScript modules that extend Pi's behavior. They run with the user's full permissions, so treat them like normal application code.
|
|
4
4
|
|
|
5
5
|
## Placement & Discovery
|
|
6
6
|
|
|
7
|
-
| Location | Scope |
|
|
8
|
-
|
|
9
|
-
| `~/.pi/agent/extensions/*.ts` | Global
|
|
10
|
-
|
|
|
11
|
-
|
|
|
12
|
-
|
|
|
7
|
+
| Location | Scope |
|
|
8
|
+
|----------|-------|
|
|
9
|
+
| `~/.pi/agent/extensions/*.ts` | Global |
|
|
10
|
+
| `~/.pi/agent/extensions/*/index.ts` | Global |
|
|
11
|
+
| `.pi/extensions/*.ts` | Project |
|
|
12
|
+
| `.pi/extensions/*/index.ts` | Project |
|
|
13
|
+
| `settings.json` → `extensions` | Additional local paths |
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
Use `pi -e ./my-extension.ts` or `pi --extension ...` for quick tests. Put stable extensions in auto-discovered locations if you want `/reload` to pick them up.
|
|
15
16
|
|
|
16
|
-
##
|
|
17
|
+
## Common Imports
|
|
17
18
|
|
|
18
19
|
```typescript
|
|
19
20
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
21
|
+
import { Type } from "@sinclair/typebox";
|
|
22
|
+
import { StringEnum } from "@mariozechner/pi-ai";
|
|
23
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- Use `@sinclair/typebox` for schemas
|
|
27
|
+
- Use `StringEnum` from `@mariozechner/pi-ai` for Google-compatible string enums
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
33
|
+
import { Type } from "@sinclair/typebox";
|
|
20
34
|
|
|
21
35
|
export default function (pi: ExtensionAPI) {
|
|
22
|
-
// Subscribe to lifecycle events
|
|
23
36
|
pi.on("session_start", async (_event, ctx) => {
|
|
24
|
-
ctx.ui.notify("Extension loaded
|
|
37
|
+
if (ctx.hasUI) ctx.ui.notify("Extension loaded", "info");
|
|
25
38
|
});
|
|
26
39
|
|
|
27
|
-
// Register a tool the LLM can call
|
|
28
40
|
pi.registerTool({
|
|
29
41
|
name: "greet",
|
|
30
42
|
label: "Greet",
|
|
@@ -32,312 +44,216 @@ export default function (pi: ExtensionAPI) {
|
|
|
32
44
|
parameters: Type.Object({
|
|
33
45
|
name: Type.String({ description: "Name to greet" }),
|
|
34
46
|
}),
|
|
35
|
-
async execute(_toolCallId, params
|
|
36
|
-
const { name } = params as { name: string };
|
|
47
|
+
async execute(_toolCallId, params) {
|
|
37
48
|
return {
|
|
38
|
-
content: [{ type: "text", text: `Hello, ${name}!` }],
|
|
39
|
-
details: {
|
|
49
|
+
content: [{ type: "text", text: `Hello, ${params.name}!` }],
|
|
50
|
+
details: {},
|
|
40
51
|
};
|
|
41
52
|
},
|
|
42
53
|
});
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
description: "Do something",
|
|
55
|
+
pi.registerCommand("hello", {
|
|
56
|
+
description: "Say hello",
|
|
47
57
|
async handler(args, ctx) {
|
|
48
|
-
ctx.ui.notify(`
|
|
58
|
+
if (ctx.hasUI) ctx.ui.notify(`Hello ${args || "world"}!`, "info");
|
|
49
59
|
},
|
|
50
60
|
});
|
|
51
61
|
}
|
|
52
62
|
```
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
## Lifecycle Events
|
|
57
|
-
|
|
58
|
-
### Session Events
|
|
59
|
-
|
|
60
|
-
| Event | When | Payload |
|
|
61
|
-
|-------|------|---------|
|
|
62
|
-
| `session_start` | Session begins | `{ sessionId }` |
|
|
63
|
-
| `session_switch` | User switches session | `{ sessionId }` |
|
|
64
|
-
| `session_fork` | Session forked | `{ entryId }` |
|
|
65
|
-
| `session_before_fork` | Before fork executes | `{ entryId }` |
|
|
66
|
-
| `session_shutdown` | Session closing | `{}` |
|
|
67
|
-
| `session_tree` | After `/tree` navigation | `{ entryId }` |
|
|
68
|
-
| `session_before_tree` | Before `/tree` navigation | `{ entryId, messages }` |
|
|
69
|
-
| `session_before_compact` | Before compaction | `{ messages, settings, ... }` |
|
|
70
|
-
| `session_compact` | After compaction | `{ summary }` |
|
|
71
|
-
| `resources_discover` | Resource discovery phase | `{ extensions, skills, prompts, themes }` |
|
|
64
|
+
## Event Surface
|
|
72
65
|
|
|
73
|
-
|
|
66
|
+
Useful session events:
|
|
74
67
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
68
|
+
- `session_start`
|
|
69
|
+
- `session_before_switch`
|
|
70
|
+
- `session_before_fork`
|
|
71
|
+
- `session_before_compact`
|
|
72
|
+
- `session_compact`
|
|
73
|
+
- `session_before_tree`
|
|
74
|
+
- `session_tree`
|
|
75
|
+
- `session_shutdown`
|
|
76
|
+
- `resources_discover`
|
|
82
77
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
| Event | When | Payload |
|
|
86
|
-
|-------|------|---------|
|
|
87
|
-
| `tool_call` | Before tool executes | `{ toolName, input }` — return `{ block, reason }` to prevent |
|
|
88
|
-
| `tool_result` | After tool executes | `{ toolName, result }` |
|
|
89
|
-
| `tool_execution_start` | Tool execution begins | `{ toolCallId, toolName }` |
|
|
90
|
-
| `tool_execution_end` | Tool execution ends | `{ toolCallId, toolName }` |
|
|
91
|
-
|
|
92
|
-
### Other Events
|
|
93
|
-
|
|
94
|
-
| Event | When | Payload |
|
|
95
|
-
|-------|------|---------|
|
|
96
|
-
| `input` | User submits input | `{ text }` — return modified text or `{ block }` |
|
|
97
|
-
| `model_select` | Model selector opened | `{ models }` — return filtered list |
|
|
98
|
-
|
|
99
|
-
## Event Handler Signature
|
|
78
|
+
Pi 0.65.0 removed the post-transition `session_switch` and `session_fork` events. Handle startup, reload, new, resume, and fork in `session_start` instead:
|
|
100
79
|
|
|
101
80
|
```typescript
|
|
102
|
-
pi.on("
|
|
103
|
-
|
|
104
|
-
|
|
81
|
+
pi.on("session_start", async (event, ctx) => {
|
|
82
|
+
if (event.reason === "fork" && event.previousSessionFile && ctx.hasUI) {
|
|
83
|
+
ctx.ui.notify(`Forked from ${event.previousSessionFile}`, "info");
|
|
84
|
+
}
|
|
105
85
|
});
|
|
106
86
|
```
|
|
107
87
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
88
|
+
Use `event.reason` to distinguish `"startup"`, `"reload"`, `"new"`, `"resume"`, and `"fork"`. For `"new"`, `"resume"`, and `"fork"`, `event.previousSessionFile` points at the session file being replaced.
|
|
89
|
+
|
|
90
|
+
Useful agent events:
|
|
91
|
+
|
|
92
|
+
- `input`
|
|
93
|
+
- `before_agent_start`
|
|
94
|
+
- `agent_start`
|
|
95
|
+
- `agent_end`
|
|
96
|
+
- `turn_start`
|
|
97
|
+
- `turn_end`
|
|
98
|
+
- `message_start`
|
|
99
|
+
- `message_update`
|
|
100
|
+
- `message_end`
|
|
101
|
+
- `context`
|
|
102
|
+
- `before_provider_request`
|
|
103
|
+
- `model_select`
|
|
104
|
+
|
|
105
|
+
Useful tool events:
|
|
106
|
+
|
|
107
|
+
- `tool_execution_start`
|
|
108
|
+
- `tool_call`
|
|
109
|
+
- `tool_execution_update`
|
|
110
|
+
- `tool_result`
|
|
111
|
+
- `tool_execution_end`
|
|
112
|
+
|
|
113
|
+
## Extension Context
|
|
114
|
+
|
|
115
|
+
`ctx` gives extensions access to:
|
|
116
|
+
|
|
117
|
+
- `ctx.ui` for interactive UI hooks
|
|
118
|
+
- `ctx.cwd`
|
|
119
|
+
- `ctx.session`
|
|
120
|
+
- `ctx.sessionManager`
|
|
121
|
+
- `ctx.modelRegistry`
|
|
122
|
+
- `ctx.hasUI`
|
|
123
|
+
- `ctx.isIdle()`
|
|
124
|
+
- `ctx.abort()`
|
|
125
|
+
- `ctx.getContextUsage()`
|
|
126
|
+
- `ctx.getSystemPrompt()`
|
|
124
127
|
|
|
125
128
|
## Tool Registration
|
|
126
129
|
|
|
127
130
|
```typescript
|
|
128
131
|
pi.registerTool({
|
|
129
|
-
name: "tool_name",
|
|
130
|
-
label: "Tool Name",
|
|
131
|
-
description: "What this tool does",
|
|
132
|
-
parameters: Type.Object({
|
|
132
|
+
name: "tool_name",
|
|
133
|
+
label: "Tool Name",
|
|
134
|
+
description: "What this tool does",
|
|
135
|
+
parameters: Type.Object({
|
|
133
136
|
arg1: Type.String({ description: "..." }),
|
|
134
|
-
arg2: Type.Optional(Type.Number()),
|
|
135
137
|
}),
|
|
136
138
|
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// onUpdate: callback for streaming partial results
|
|
142
|
-
// ctx: ExtensionContext
|
|
139
|
+
// Runs before schema validation. Useful for migrating old tool-call shapes.
|
|
140
|
+
prepareArguments(rawArgs) {
|
|
141
|
+
return rawArgs;
|
|
142
|
+
},
|
|
143
143
|
|
|
144
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
144
145
|
return {
|
|
145
146
|
content: [{ type: "text", text: "result" }],
|
|
146
|
-
details: {
|
|
147
|
+
details: { persisted: true },
|
|
147
148
|
};
|
|
148
149
|
},
|
|
149
|
-
|
|
150
|
-
// Optional: custom TUI rendering for tool calls
|
|
151
|
-
renderCall(args, theme, context) {
|
|
152
|
-
return new Text(theme.fg("accent", `tool ${args.arg1}`), 0, 0);
|
|
153
|
-
},
|
|
154
|
-
|
|
155
|
-
// Optional: custom TUI rendering for tool results
|
|
156
|
-
renderResult(result, { expanded }, theme, context) {
|
|
157
|
-
return new Text(result.content[0].text, 0, 0);
|
|
158
|
-
},
|
|
159
150
|
});
|
|
160
151
|
```
|
|
161
152
|
|
|
162
|
-
|
|
153
|
+
Important rules:
|
|
163
154
|
|
|
164
|
-
1.
|
|
165
|
-
2.
|
|
166
|
-
3.
|
|
167
|
-
4.
|
|
168
|
-
5.
|
|
155
|
+
1. Truncate large output.
|
|
156
|
+
2. Respect `signal.aborted`.
|
|
157
|
+
3. Throw on failure instead of returning fake success.
|
|
158
|
+
4. Use the file-mutation queue for write/edit style tools.
|
|
159
|
+
5. Put reconstructable state in `details`; it persists in session history.
|
|
169
160
|
|
|
170
161
|
## UI Methods
|
|
171
162
|
|
|
172
|
-
|
|
163
|
+
Check `ctx.hasUI` first. Non-interactive modes may not support UI.
|
|
173
164
|
|
|
174
165
|
```typescript
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
const edited = await ctx.ui.editor("Edit this text", initialContent);
|
|
166
|
+
const choice = await ctx.ui.select("Pick one:", ["A", "B"]);
|
|
167
|
+
const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
|
|
168
|
+
const text = await ctx.ui.input("Name:", "default");
|
|
169
|
+
const edited = await ctx.ui.editor("Edit:", "prefilled text");
|
|
180
170
|
|
|
181
|
-
|
|
182
|
-
ctx.ui.notify("Something happened", "info"); // "info" | "warning" | "error"
|
|
171
|
+
ctx.ui.notify("Done", "info");
|
|
183
172
|
|
|
184
|
-
|
|
185
|
-
ctx.ui.
|
|
186
|
-
ctx.ui.setWidget("
|
|
187
|
-
ctx.ui.setFooter(
|
|
173
|
+
ctx.ui.setStatus("my-ext", "Processing...");
|
|
174
|
+
ctx.ui.setWorkingMessage("Thinking deeply...");
|
|
175
|
+
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
|
|
176
|
+
ctx.ui.setFooter((tui, theme) => new Text(theme.fg("dim", "Custom footer"), 0, 0));
|
|
177
|
+
ctx.ui.setTitle("pi - custom");
|
|
188
178
|
|
|
189
|
-
// Editor control
|
|
190
179
|
ctx.ui.setEditorText("prefilled text");
|
|
191
|
-
ctx.ui.
|
|
180
|
+
const current = ctx.ui.getEditorText();
|
|
181
|
+
ctx.ui.pasteToEditor("extra text");
|
|
192
182
|
|
|
193
|
-
|
|
194
|
-
const handle = ctx.ui.custom(component, { overlay: true });
|
|
195
|
-
handle.requestRender();
|
|
196
|
-
handle.close();
|
|
197
|
-
```
|
|
183
|
+
ctx.ui.setToolsExpanded(true);
|
|
198
184
|
|
|
199
|
-
|
|
185
|
+
const result = await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
186
|
+
return new Text("Press Enter", 0, 0);
|
|
187
|
+
}, { overlay: true });
|
|
188
|
+
```
|
|
200
189
|
|
|
201
|
-
|
|
202
|
-
pi.registerCommand("commandname", {
|
|
203
|
-
description: "What this command does", // Shown in autocomplete
|
|
204
|
-
args: "optional args description",
|
|
190
|
+
Pi 0.64.0 also added `ctx.ui.setHiddenThinkingLabel(...)` so interactive extensions can customize the collapsed thinking label.
|
|
205
191
|
|
|
206
|
-
|
|
207
|
-
complete(partial) {
|
|
208
|
-
return ["suggestion1", "suggestion2"].filter(s => s.startsWith(partial));
|
|
209
|
-
},
|
|
192
|
+
## Commands, Shortcuts, Flags
|
|
210
193
|
|
|
211
|
-
|
|
194
|
+
```typescript
|
|
195
|
+
pi.registerCommand("name", {
|
|
196
|
+
description: "Run a command",
|
|
212
197
|
async handler(args, ctx) {
|
|
213
|
-
await ctx.waitForIdle();
|
|
214
|
-
ctx.
|
|
215
|
-
ctx.fork(entryId); // Fork from entry
|
|
216
|
-
ctx.navigateTree(); // Open tree navigator
|
|
217
|
-
ctx.reload(); // Reload extensions
|
|
198
|
+
await ctx.waitForIdle();
|
|
199
|
+
ctx.reload();
|
|
218
200
|
},
|
|
219
201
|
});
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
## Keybinding Registration
|
|
223
202
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
description: "Do something",
|
|
228
|
-
handler: async (ctx) => { /* ... */ },
|
|
203
|
+
pi.registerShortcut("ctrl+x", {
|
|
204
|
+
description: "Do something quickly",
|
|
205
|
+
handler: async (ctx) => {},
|
|
229
206
|
});
|
|
230
|
-
```
|
|
231
207
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
```typescript
|
|
235
|
-
pi.registerFlag({
|
|
236
|
-
name: "--my-flag",
|
|
208
|
+
pi.registerFlag("my-flag", {
|
|
237
209
|
description: "Enable something",
|
|
238
|
-
handler: (value) => {
|
|
210
|
+
handler: (value) => {},
|
|
239
211
|
});
|
|
240
212
|
```
|
|
241
213
|
|
|
242
|
-
##
|
|
214
|
+
## Persistence & Process Hooks
|
|
243
215
|
|
|
244
|
-
|
|
216
|
+
Store extension state with custom session entries:
|
|
245
217
|
|
|
246
218
|
```typescript
|
|
247
|
-
// Write state
|
|
248
219
|
pi.appendEntry("my-extension-state", { key: "value" });
|
|
220
|
+
```
|
|
249
221
|
|
|
250
|
-
|
|
222
|
+
Use lifecycle hooks to restore it:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
251
225
|
pi.on("session_start", async (_event, ctx) => {
|
|
252
|
-
const
|
|
253
|
-
for (const entry of branch.reverse()) {
|
|
226
|
+
for (const entry of ctx.sessionManager.getBranch().reverse()) {
|
|
254
227
|
if (entry.type === "custom" && entry.name === "my-extension-state") {
|
|
255
|
-
const state = entry.data;
|
|
256
|
-
// Restore from state
|
|
257
228
|
break;
|
|
258
229
|
}
|
|
259
230
|
}
|
|
260
231
|
});
|
|
261
232
|
```
|
|
262
233
|
|
|
263
|
-
## Provider
|
|
234
|
+
## Provider Integration
|
|
264
235
|
|
|
265
|
-
Extensions can register
|
|
236
|
+
Extensions can register providers:
|
|
266
237
|
|
|
267
238
|
```typescript
|
|
268
239
|
pi.registerProvider("my-provider", {
|
|
269
240
|
baseUrl: "https://api.example.com",
|
|
270
241
|
api: "openai-completions",
|
|
271
|
-
apiKey: "
|
|
272
|
-
models: [
|
|
273
|
-
{ id: "my-model", name: "My Model", contextWindow: 128000, maxTokens: 4096 },
|
|
274
|
-
],
|
|
275
|
-
});
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
For full custom streaming, implement `streamSimple` on the provider config. See `references/providers.md`.
|
|
279
|
-
|
|
280
|
-
## Bash Execution
|
|
281
|
-
|
|
282
|
-
```typescript
|
|
283
|
-
const { stdout, stderr, exitCode } = await pi.exec("git", ["status"]);
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
## Practical Examples
|
|
287
|
-
|
|
288
|
-
### Permission Gate
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
292
|
-
if (event.toolName !== "bash") return;
|
|
293
|
-
const cmd = event.input.command as string;
|
|
294
|
-
if (/\brm\s+(-rf?|--recursive)/i.test(cmd)) {
|
|
295
|
-
if (!ctx.hasUI) return { block: true, reason: "Dangerous command blocked" };
|
|
296
|
-
const ok = await ctx.ui.select(`⚠️ Dangerous: ${cmd}\nAllow?`, ["Yes", "No"]);
|
|
297
|
-
if (ok !== "Yes") return { block: true, reason: "Blocked by user" };
|
|
298
|
-
}
|
|
242
|
+
apiKey: "MY_PROVIDER_KEY",
|
|
243
|
+
models: [{ id: "my-model", name: "My Model", contextWindow: 128000, maxTokens: 4096 }],
|
|
299
244
|
});
|
|
300
245
|
```
|
|
301
246
|
|
|
302
|
-
|
|
247
|
+
If you need auth for a specific model request, use:
|
|
303
248
|
|
|
304
249
|
```typescript
|
|
305
|
-
|
|
306
|
-
event.systemPrompt += "\n\nAdditional instructions here.";
|
|
307
|
-
});
|
|
250
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
308
251
|
```
|
|
309
252
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
```typescript
|
|
313
|
-
pi.on("turn_start", async () => {
|
|
314
|
-
const { stdout } = await pi.exec("git", ["stash", "create"]);
|
|
315
|
-
if (stdout.trim()) checkpoints.set(currentEntryId, stdout.trim());
|
|
316
|
-
});
|
|
317
|
-
```
|
|
253
|
+
## Handy Patterns
|
|
318
254
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const allMessages = [...event.messagesToSummarize, ...event.turnPrefixMessages];
|
|
325
|
-
const summary = await generateCustomSummary(allMessages);
|
|
326
|
-
return { summary, firstKeptEntryId: event.firstKeptEntryId, tokensBefore: event.tokens };
|
|
327
|
-
});
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
### Dynamic Tool Registration
|
|
331
|
-
|
|
332
|
-
```typescript
|
|
333
|
-
pi.on("session_start", async () => {
|
|
334
|
-
pi.registerTool({ name: "echo", /* ... */ });
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
pi.registerCommand("add-tool", {
|
|
338
|
-
async handler(name, ctx) {
|
|
339
|
-
pi.registerTool({ name, /* ... */ });
|
|
340
|
-
ctx.ui.notify(`Registered tool: ${name}`, "info");
|
|
341
|
-
},
|
|
342
|
-
});
|
|
343
|
-
```
|
|
255
|
+
- Permission gates via `tool_call`
|
|
256
|
+
- Prompt injection via `before_agent_start`
|
|
257
|
+
- Branch or compaction customization via `session_before_tree` / `session_before_compact`
|
|
258
|
+
- Provider payload inspection via `before_provider_request`
|
|
259
|
+
- Stateful tools via `details` plus `session_start` reconstruction
|