@jhytabest/plashboard 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/README.md CHANGED
@@ -25,6 +25,7 @@ No manual config is required for first use. Defaults are safe:
25
25
  - `fill_provider=openclaw`
26
26
  - `allow_command_fill=false`
27
27
  - `openclaw_fill_agent_id=main`
28
+ - `session_strategy=persistent`
28
29
  - automatic init on service start
29
30
  - automatic starter template seed when template store is empty
30
31
 
@@ -58,6 +59,7 @@ Add to `openclaw.json`:
58
59
  "fill_provider": "openclaw",
59
60
  "allow_command_fill": false,
60
61
  "openclaw_fill_agent_id": "main",
62
+ "session_strategy": "persistent",
61
63
  "display_profile": {
62
64
  "width_px": 1920,
63
65
  "height_px": 1080,
@@ -77,6 +79,33 @@ Add to `openclaw.json`:
77
79
  `fill_provider: "command"` requires explicit opt-in with `allow_command_fill: true`.
78
80
  Use command mode only if you need a custom external runner.
79
81
 
82
+ `session_strategy` controls OpenClaw session reuse for fills:
83
+ - `persistent` (default): reuses the agent's normal long-lived session behavior.
84
+ - `ephemeral`: each fill run gets a unique `--session-id`; after the run, plugin performs best-effort cleanup via official CLI API: `openclaw sessions delete --agent <id> --session-id <id>`.
85
+
86
+ Tradeoffs:
87
+ - `persistent` keeps conversational memory/context between runs.
88
+ - `ephemeral` isolates runs and avoids long-lived context drift, but loses cross-run memory and adds one cleanup CLI call per run.
89
+
90
+ Example ephemeral config:
91
+
92
+ ```json
93
+ {
94
+ "plugins": {
95
+ "entries": {
96
+ "plashboard": {
97
+ "enabled": true,
98
+ "config": {
99
+ "fill_provider": "openclaw",
100
+ "openclaw_fill_agent_id": "plashboard-fill",
101
+ "session_strategy": "ephemeral"
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
80
109
  For production stability, use a dedicated fill agent instead of `main`:
81
110
 
82
111
  ```bash
@@ -20,6 +20,7 @@
20
20
  "allow_command_fill": { "type": "boolean", "default": false },
21
21
  "fill_command": { "type": "string" },
22
22
  "openclaw_fill_agent_id": { "type": "string", "default": "main" },
23
+ "session_strategy": { "type": "string", "enum": ["persistent", "ephemeral"], "default": "persistent" },
23
24
  "python_bin": { "type": "string", "default": "python3" },
24
25
  "writer_script_path": { "type": "string" },
25
26
  "dashboard_output_path": { "type": "string" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhytabest/plashboard",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "private": false,
5
5
  "description": "Plashboard OpenClaw plugin runtime",
6
6
  "license": "MIT",
package/src/config.ts CHANGED
@@ -38,6 +38,10 @@ function asObject(value: unknown): Record<string, unknown> {
38
38
  : {};
39
39
  }
40
40
 
41
+ function asSessionStrategy(value: unknown): PlashboardConfig['session_strategy'] {
42
+ return value === 'ephemeral' ? 'ephemeral' : 'persistent';
43
+ }
44
+
41
45
  function resolveDisplayProfile(raw: unknown): DisplayProfile {
42
46
  const data = asObject(raw);
43
47
  return {
@@ -80,6 +84,7 @@ export function resolveConfig(api: unknown): PlashboardConfig {
80
84
  allow_command_fill: asBoolean(raw.allow_command_fill, false),
81
85
  fill_command: typeof raw.fill_command === 'string' ? raw.fill_command : undefined,
82
86
  openclaw_fill_agent_id: asString(raw.openclaw_fill_agent_id, 'main'),
87
+ session_strategy: asSessionStrategy(raw.session_strategy),
83
88
  python_bin: asString(raw.python_bin, 'python3'),
84
89
  writer_script_path: asString(raw.writer_script_path, DEFAULT_WRITER_PATH),
85
90
  dashboard_output_path: outputPath,
@@ -42,6 +42,7 @@ function config(overrides: Partial<PlashboardConfig>): PlashboardConfig {
42
42
  allow_command_fill: false,
43
43
  fill_command: undefined,
44
44
  openclaw_fill_agent_id: 'main',
45
+ session_strategy: 'persistent',
45
46
  python_bin: 'python3',
46
47
  writer_script_path: '/tmp/writer.py',
47
48
  dashboard_output_path: '/tmp/dashboard.json',
@@ -68,7 +69,7 @@ function context(): FillRunContext {
68
69
  }
69
70
 
70
71
  describe('createFillRunner', () => {
71
- it('parses openclaw json envelope output', async () => {
72
+ it('persistent mode keeps standard openclaw agent session behavior', async () => {
72
73
  const commandRunner = vi.fn(async (_argv: string[], _options: unknown) => ({
73
74
  stdout: JSON.stringify({
74
75
  result: {
@@ -96,6 +97,108 @@ describe('createFillRunner', () => {
96
97
  expect(argv.slice(0, 2)).toEqual(['openclaw', 'agent']);
97
98
  expect(argv).toContain('--agent');
98
99
  expect(argv).toContain('ops');
100
+ expect(argv).not.toContain('--session-id');
101
+ });
102
+
103
+ it('ephemeral mode uses unique session ids and official cleanup command', async () => {
104
+ const calls: string[][] = [];
105
+ const commandRunner = vi.fn(async (argv: string[], _options: unknown) => {
106
+ calls.push(argv);
107
+ if (argv[0] === 'openclaw' && argv[1] === 'agent') {
108
+ return {
109
+ stdout: '{"values":{"summary":"new summary"}}',
110
+ stderr: '',
111
+ code: 0
112
+ };
113
+ }
114
+ if (argv[0] === 'openclaw' && argv[1] === 'sessions' && argv[2] === 'delete') {
115
+ return {
116
+ stdout: '{"ok":true}',
117
+ stderr: '',
118
+ code: 0
119
+ };
120
+ }
121
+ return {
122
+ stdout: '',
123
+ stderr: `unsupported command: ${argv.join(' ')}`,
124
+ code: 1
125
+ };
126
+ });
127
+
128
+ const runner = createFillRunner(
129
+ config({
130
+ fill_provider: 'openclaw',
131
+ openclaw_fill_agent_id: 'ops',
132
+ session_strategy: 'ephemeral'
133
+ }),
134
+ { commandRunner }
135
+ );
136
+
137
+ await runner.run(context());
138
+ await runner.run(context());
139
+
140
+ const agentCalls = calls.filter((argv) => argv[0] === 'openclaw' && argv[1] === 'agent');
141
+ const cleanupCalls = calls.filter((argv) => argv[0] === 'openclaw' && argv[1] === 'sessions' && argv[2] === 'delete');
142
+
143
+ expect(agentCalls).toHaveLength(2);
144
+ expect(cleanupCalls).toHaveLength(2);
145
+
146
+ const firstSessionFlagIndex = agentCalls[0].indexOf('--session-id');
147
+ const secondSessionFlagIndex = agentCalls[1].indexOf('--session-id');
148
+ expect(firstSessionFlagIndex).toBeGreaterThan(-1);
149
+ expect(secondSessionFlagIndex).toBeGreaterThan(-1);
150
+ const firstSessionId = agentCalls[0][firstSessionFlagIndex + 1];
151
+ const secondSessionId = agentCalls[1][secondSessionFlagIndex + 1];
152
+ expect(firstSessionId).toBeTruthy();
153
+ expect(secondSessionId).toBeTruthy();
154
+ expect(firstSessionId).not.toBe(secondSessionId);
155
+
156
+ for (const [index, cleanupCall] of cleanupCalls.entries()) {
157
+ expect(cleanupCall.slice(0, 3)).toEqual(['openclaw', 'sessions', 'delete']);
158
+ expect(cleanupCall).toContain('--agent');
159
+ expect(cleanupCall).toContain('ops');
160
+ const cleanupSessionFlagIndex = cleanupCall.indexOf('--session-id');
161
+ expect(cleanupSessionFlagIndex).toBeGreaterThan(-1);
162
+ expect(cleanupCall[cleanupSessionFlagIndex + 1]).toBe(index === 0 ? firstSessionId : secondSessionId);
163
+ }
164
+ });
165
+
166
+ it('ephemeral cleanup failure is safe and does not fail fill output', async () => {
167
+ const commandRunner = vi.fn(async (argv: string[], _options: unknown) => {
168
+ if (argv[0] === 'openclaw' && argv[1] === 'agent') {
169
+ return {
170
+ stdout: '{"values":{"summary":"new summary"}}',
171
+ stderr: '',
172
+ code: 0
173
+ };
174
+ }
175
+ if (argv[0] === 'openclaw' && argv[1] === 'sessions' && argv[2] === 'delete') {
176
+ return {
177
+ stdout: '',
178
+ stderr: 'session cleanup failed',
179
+ code: 1
180
+ };
181
+ }
182
+ return {
183
+ stdout: '',
184
+ stderr: `unsupported command: ${argv.join(' ')}`,
185
+ code: 1
186
+ };
187
+ });
188
+
189
+ const runner = createFillRunner(
190
+ config({
191
+ fill_provider: 'openclaw',
192
+ openclaw_fill_agent_id: 'ops',
193
+ session_strategy: 'ephemeral'
194
+ }),
195
+ { commandRunner }
196
+ );
197
+ const response = await runner.run(context());
198
+
199
+ expect(response.values.summary).toBe('new summary');
200
+ expect(commandRunner).toHaveBeenCalledTimes(2);
201
+ expect(commandRunner.mock.calls[1][0].slice(0, 3)).toEqual(['openclaw', 'sessions', 'delete']);
99
202
  });
100
203
 
101
204
  it('parses command runner fenced json output', async () => {
@@ -1,10 +1,12 @@
1
- import { runAndReadStdout, type CommandRunner } from './command-runner.js';
1
+ import { runAndReadStdout, runCommand, type CommandRunner } from './command-runner.js';
2
2
  import type { FillResponse, FillRunContext, FillRunner, PlashboardConfig } from './types.js';
3
3
 
4
4
  export interface FillRunnerDeps {
5
5
  commandRunner?: CommandRunner | null;
6
6
  }
7
7
 
8
+ let ephemeralSessionCounter = 0;
9
+
8
10
  function buildPromptPayload(context: FillRunContext): Record<string, unknown> {
9
11
  return {
10
12
  instructions: {
@@ -142,6 +144,25 @@ function parseFillResponse(output: string, source: string): FillResponse {
142
144
  return extracted;
143
145
  }
144
146
 
147
+ function sanitizeSessionToken(input: string): string {
148
+ return input.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24) || 'x';
149
+ }
150
+
151
+ function nextEphemeralSessionCounter(): number {
152
+ ephemeralSessionCounter += 1;
153
+ return ephemeralSessionCounter;
154
+ }
155
+
156
+ function buildEphemeralSessionId(agentId: string, context: FillRunContext): string {
157
+ const templateId = sanitizeSessionToken(context.template.id || 'template');
158
+ const agent = sanitizeSessionToken(agentId || 'agent');
159
+ const attempt = Math.max(1, Math.floor(context.attempt || 1));
160
+ const now = Date.now().toString(36);
161
+ const pid = process.pid.toString(36);
162
+ const seq = nextEphemeralSessionCounter().toString(36);
163
+ return `plash-${agent}-${templateId}-a${attempt}-${pid}-${now}-${seq}`;
164
+ }
165
+
145
166
  class MockFillRunner implements FillRunner {
146
167
  async run(context: FillRunContext): Promise<FillResponse> {
147
168
  const values: Record<string, unknown> = {};
@@ -192,17 +213,37 @@ class OpenClawFillRunner implements FillRunner {
192
213
  const agentId = (this.config.openclaw_fill_agent_id || 'main').trim() || 'main';
193
214
  const timeoutSeconds = Math.max(10, Math.floor(this.config.session_timeout_seconds));
194
215
  const message = buildOpenClawMessage(context);
216
+ const ephemeral = this.config.session_strategy === 'ephemeral';
217
+ const sessionId = ephemeral ? buildEphemeralSessionId(agentId, context) : undefined;
218
+ const argv = ['openclaw', 'agent', '--agent', agentId, '--message', message, '--json', '--timeout', String(timeoutSeconds)];
219
+ if (sessionId) {
220
+ argv.push('--session-id', sessionId);
221
+ }
195
222
 
196
- const output = await runAndReadStdout(
197
- this.commandRunner,
198
- ['openclaw', 'agent', '--agent', agentId, '--message', message, '--json', '--timeout', String(timeoutSeconds)],
199
- {
200
- timeoutMs: (timeoutSeconds + 30) * 1000
201
- },
202
- 'openclaw fill'
203
- );
223
+ try {
224
+ const output = await runAndReadStdout(
225
+ this.commandRunner,
226
+ argv,
227
+ {
228
+ timeoutMs: (timeoutSeconds + 30) * 1000
229
+ },
230
+ 'openclaw fill'
231
+ );
204
232
 
205
- return parseFillResponse(output, 'openclaw fill');
233
+ return parseFillResponse(output, 'openclaw fill');
234
+ } finally {
235
+ if (ephemeral && sessionId) {
236
+ // Best-effort cleanup through official CLI API; never mutate session files directly.
237
+ await runCommand(
238
+ this.commandRunner,
239
+ ['openclaw', 'sessions', 'delete', '--agent', agentId, '--session-id', sessionId, '--json'],
240
+ {
241
+ timeoutMs: Math.max(5, timeoutSeconds) * 1000
242
+ },
243
+ 'openclaw ephemeral session cleanup'
244
+ );
245
+ }
246
+ }
206
247
  }
207
248
  }
208
249
 
package/src/plugin.ts CHANGED
@@ -80,6 +80,7 @@ type SetupParams = {
80
80
  allow_command_fill?: boolean;
81
81
  fill_command?: string;
82
82
  openclaw_fill_agent_id?: string;
83
+ session_strategy?: 'persistent' | 'ephemeral';
83
84
  auto_seed_template?: boolean;
84
85
  data_dir?: string;
85
86
  scheduler_tick_seconds?: number;
@@ -514,6 +515,10 @@ async function runSetup(
514
515
  || asString(resolvedConfig.openclaw_fill_agent_id)
515
516
  || 'main'
516
517
  ).trim();
518
+ const rawSessionStrategy = asString(params.session_strategy)
519
+ || asString(currentPluginConfig.session_strategy)
520
+ || asString(resolvedConfig.session_strategy);
521
+ const selectedSessionStrategy = rawSessionStrategy === 'ephemeral' ? 'ephemeral' : 'persistent';
517
522
 
518
523
  if (selectedProvider === 'command' && !selectedCommand) {
519
524
  return {
@@ -587,6 +592,7 @@ async function runSetup(
587
592
  ),
588
593
  fill_provider: selectedProvider,
589
594
  allow_command_fill: selectedAllowCommandFill,
595
+ session_strategy: selectedSessionStrategy,
590
596
  auto_seed_template: selectedAutoSeed,
591
597
  display_profile: displayProfile
592
598
  };
@@ -630,6 +636,7 @@ async function runSetup(
630
636
  allow_command_fill: selectedAllowCommandFill,
631
637
  fill_command: selectedProvider === 'command' ? selectedCommand : undefined,
632
638
  openclaw_fill_agent_id: selectedProvider === 'openclaw' ? selectedAgentId : undefined,
639
+ session_strategy: selectedSessionStrategy,
633
640
  auto_seed_template: selectedAutoSeed,
634
641
  data_dir: nextPluginConfig.data_dir,
635
642
  scheduler_tick_seconds: nextPluginConfig.scheduler_tick_seconds,
@@ -1064,6 +1071,7 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
1064
1071
  allow_command_fill: { type: 'boolean' },
1065
1072
  fill_command: { type: 'string' },
1066
1073
  openclaw_fill_agent_id: { type: 'string' },
1074
+ session_strategy: { type: 'string', enum: ['persistent', 'ephemeral'] },
1067
1075
  auto_seed_template: { type: 'boolean' },
1068
1076
  data_dir: { type: 'string' },
1069
1077
  scheduler_tick_seconds: { type: 'number' },
@@ -66,6 +66,7 @@ async function setupRuntime(overrides: Partial<PlashboardConfig> = {}) {
66
66
  fill_provider: 'mock',
67
67
  allow_command_fill: false,
68
68
  fill_command: undefined,
69
+ session_strategy: 'persistent',
69
70
  python_bin: 'python3',
70
71
  writer_script_path: join(process.cwd(), 'scripts', 'dashboard_write.py'),
71
72
  dashboard_output_path: join(root, 'dashboard.json'),
package/src/types.ts CHANGED
@@ -15,6 +15,8 @@ export interface ModelDefaults {
15
15
  max_tokens?: number;
16
16
  }
17
17
 
18
+ export type SessionStrategy = 'persistent' | 'ephemeral';
19
+
18
20
  export interface PlashboardConfig {
19
21
  data_dir: string;
20
22
  timezone: string;
@@ -28,6 +30,7 @@ export interface PlashboardConfig {
28
30
  allow_command_fill: boolean;
29
31
  fill_command?: string;
30
32
  openclaw_fill_agent_id?: string;
33
+ session_strategy: SessionStrategy;
31
34
  python_bin: string;
32
35
  writer_script_path: string;
33
36
  dashboard_output_path: string;