@jhytabest/plashboard 1.0.1 → 1.0.2

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,7 +25,6 @@ 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`
29
28
  - automatic init on service start
30
29
  - automatic starter template seed when template store is empty
31
30
 
@@ -59,7 +58,6 @@ Add to `openclaw.json`:
59
58
  "fill_provider": "openclaw",
60
59
  "allow_command_fill": false,
61
60
  "openclaw_fill_agent_id": "main",
62
- "session_strategy": "persistent",
63
61
  "display_profile": {
64
62
  "width_px": 1920,
65
63
  "height_px": 1080,
@@ -79,32 +77,11 @@ Add to `openclaw.json`:
79
77
  `fill_provider: "command"` requires explicit opt-in with `allow_command_fill: true`.
80
78
  Use command mode only if you need a custom external runner.
81
79
 
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>`.
80
+ OpenClaw fill sessions are always cleaned through official Gateway API calls:
81
+ - Pre-run: `openclaw gateway call sessions.reset --params '{"key":"agent:<fill_agent_id>:main","reason":"new"}'`
82
+ - Post-run: same reset call as best-effort cleanup.
85
83
 
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
- ```
84
+ This keeps fill runs stateless without editing OpenClaw session files directly.
108
85
 
109
86
  For production stability, use a dedicated fill agent instead of `main`:
110
87
 
@@ -20,7 +20,6 @@
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" },
24
23
  "python_bin": { "type": "string", "default": "python3" },
25
24
  "writer_script_path": { "type": "string" },
26
25
  "dashboard_output_path": { "type": "string" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhytabest/plashboard",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "private": false,
5
5
  "description": "Plashboard OpenClaw plugin runtime",
6
6
  "license": "MIT",
package/src/config.ts CHANGED
@@ -38,10 +38,6 @@ 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
-
45
41
  function resolveDisplayProfile(raw: unknown): DisplayProfile {
46
42
  const data = asObject(raw);
47
43
  return {
@@ -84,7 +80,6 @@ export function resolveConfig(api: unknown): PlashboardConfig {
84
80
  allow_command_fill: asBoolean(raw.allow_command_fill, false),
85
81
  fill_command: typeof raw.fill_command === 'string' ? raw.fill_command : undefined,
86
82
  openclaw_fill_agent_id: asString(raw.openclaw_fill_agent_id, 'main'),
87
- session_strategy: asSessionStrategy(raw.session_strategy),
88
83
  python_bin: asString(raw.python_bin, 'python3'),
89
84
  writer_script_path: asString(raw.writer_script_path, DEFAULT_WRITER_PATH),
90
85
  dashboard_output_path: outputPath,
@@ -42,7 +42,6 @@ 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',
46
45
  python_bin: 'python3',
47
46
  writer_script_path: '/tmp/writer.py',
48
47
  dashboard_output_path: '/tmp/dashboard.json',
@@ -69,20 +68,38 @@ function context(): FillRunContext {
69
68
  }
70
69
 
71
70
  describe('createFillRunner', () => {
72
- it('persistent mode keeps standard openclaw agent session behavior', async () => {
73
- const commandRunner = vi.fn(async (_argv: string[], _options: unknown) => ({
74
- stdout: JSON.stringify({
75
- result: {
76
- payloads: [
77
- {
78
- text: '{"values":{"summary":"new summary"}}'
71
+ it('always resets fill session before and after openclaw fill', async () => {
72
+ const calls: string[][] = [];
73
+ const commandRunner = vi.fn(async (argv: string[], _options: unknown) => {
74
+ calls.push(argv);
75
+ if (argv[0] === 'openclaw' && argv[1] === 'gateway' && argv[2] === 'call' && argv[3] === 'sessions.reset') {
76
+ return {
77
+ stdout: '{"ok":true}',
78
+ stderr: '',
79
+ code: 0
80
+ };
81
+ }
82
+ if (argv[0] === 'openclaw' && argv[1] === 'agent') {
83
+ return {
84
+ stdout: JSON.stringify({
85
+ result: {
86
+ payloads: [
87
+ {
88
+ text: '{"values":{"summary":"new summary"}}'
89
+ }
90
+ ]
79
91
  }
80
- ]
81
- }
82
- }),
83
- stderr: '',
84
- code: 0
85
- }));
92
+ }),
93
+ stderr: '',
94
+ code: 0
95
+ };
96
+ }
97
+ return {
98
+ stdout: '',
99
+ stderr: `unsupported command: ${argv.join(' ')}`,
100
+ code: 1
101
+ };
102
+ });
86
103
 
87
104
  const runner = createFillRunner(
88
105
  config({ fill_provider: 'openclaw', openclaw_fill_agent_id: 'ops' }),
@@ -91,31 +108,33 @@ describe('createFillRunner', () => {
91
108
  const response = await runner.run(context());
92
109
 
93
110
  expect(response.values.summary).toBe('new summary');
94
- expect(commandRunner).toHaveBeenCalledTimes(1);
95
- const firstCall = commandRunner.mock.calls[0];
96
- const argv = firstCall[0];
97
- expect(argv.slice(0, 2)).toEqual(['openclaw', 'agent']);
98
- expect(argv).toContain('--agent');
99
- expect(argv).toContain('ops');
100
- expect(argv).not.toContain('--session-id');
111
+ expect(commandRunner).toHaveBeenCalledTimes(3);
112
+
113
+ const [firstCall, secondCall, thirdCall] = calls;
114
+ expect(firstCall.slice(0, 4)).toEqual(['openclaw', 'gateway', 'call', 'sessions.reset']);
115
+ expect(secondCall.slice(0, 2)).toEqual(['openclaw', 'agent']);
116
+ expect(thirdCall.slice(0, 4)).toEqual(['openclaw', 'gateway', 'call', 'sessions.reset']);
117
+
118
+ expect(secondCall).toContain('--agent');
119
+ expect(secondCall).toContain('ops');
120
+ expect(secondCall).not.toContain('--session-id');
121
+
122
+ for (const resetCall of [firstCall, thirdCall]) {
123
+ const paramsIndex = resetCall.indexOf('--params');
124
+ expect(paramsIndex).toBeGreaterThan(-1);
125
+ const params = JSON.parse(resetCall[paramsIndex + 1]) as { key?: string; reason?: string };
126
+ expect(params.key).toBe('agent:ops:main');
127
+ expect(params.reason).toBe('new');
128
+ }
101
129
  });
102
130
 
103
- it('ephemeral mode uses unique session ids and official cleanup command', async () => {
104
- const calls: string[][] = [];
131
+ it('fails fill when pre-run session reset fails', async () => {
105
132
  const commandRunner = vi.fn(async (argv: string[], _options: unknown) => {
106
- calls.push(argv);
107
- if (argv[0] === 'openclaw' && argv[1] === 'agent') {
133
+ if (argv[0] === 'openclaw' && argv[1] === 'gateway' && argv[2] === 'call' && argv[3] === 'sessions.reset') {
108
134
  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
135
+ stdout: '',
136
+ stderr: 'reset failed',
137
+ code: 1
119
138
  };
120
139
  }
121
140
  return {
@@ -128,43 +147,33 @@ describe('createFillRunner', () => {
128
147
  const runner = createFillRunner(
129
148
  config({
130
149
  fill_provider: 'openclaw',
131
- openclaw_fill_agent_id: 'ops',
132
- session_strategy: 'ephemeral'
150
+ openclaw_fill_agent_id: 'ops'
133
151
  }),
134
152
  { commandRunner }
135
153
  );
136
154
 
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
- }
155
+ await expect(runner.run(context())).rejects.toThrow(/session reset failed/i);
156
+ expect(commandRunner).toHaveBeenCalledTimes(1);
164
157
  });
165
158
 
166
- it('ephemeral cleanup failure is safe and does not fail fill output', async () => {
159
+ it('post-run session reset failure is safe and does not fail fill output', async () => {
160
+ let resetCalls = 0;
167
161
  const commandRunner = vi.fn(async (argv: string[], _options: unknown) => {
162
+ if (argv[0] === 'openclaw' && argv[1] === 'gateway' && argv[2] === 'call' && argv[3] === 'sessions.reset') {
163
+ resetCalls += 1;
164
+ if (resetCalls === 1) {
165
+ return {
166
+ stdout: '{"ok":true}',
167
+ stderr: '',
168
+ code: 0
169
+ };
170
+ }
171
+ return {
172
+ stdout: '',
173
+ stderr: 'reset failed',
174
+ code: 1
175
+ };
176
+ }
168
177
  if (argv[0] === 'openclaw' && argv[1] === 'agent') {
169
178
  return {
170
179
  stdout: '{"values":{"summary":"new summary"}}',
@@ -172,13 +181,6 @@ describe('createFillRunner', () => {
172
181
  code: 0
173
182
  };
174
183
  }
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
184
  return {
183
185
  stdout: '',
184
186
  stderr: `unsupported command: ${argv.join(' ')}`,
@@ -189,16 +191,15 @@ describe('createFillRunner', () => {
189
191
  const runner = createFillRunner(
190
192
  config({
191
193
  fill_provider: 'openclaw',
192
- openclaw_fill_agent_id: 'ops',
193
- session_strategy: 'ephemeral'
194
+ openclaw_fill_agent_id: 'ops'
194
195
  }),
195
196
  { commandRunner }
196
197
  );
197
198
  const response = await runner.run(context());
198
199
 
199
200
  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']);
201
+ expect(commandRunner).toHaveBeenCalledTimes(3);
202
+ expect(commandRunner.mock.calls[2][0].slice(0, 4)).toEqual(['openclaw', 'gateway', 'call', 'sessions.reset']);
202
203
  });
203
204
 
204
205
  it('parses command runner fenced json output', async () => {
@@ -5,8 +5,6 @@ export interface FillRunnerDeps {
5
5
  commandRunner?: CommandRunner | null;
6
6
  }
7
7
 
8
- let ephemeralSessionCounter = 0;
9
-
10
8
  function buildPromptPayload(context: FillRunContext): Record<string, unknown> {
11
9
  return {
12
10
  instructions: {
@@ -144,23 +142,67 @@ function parseFillResponse(output: string, source: string): FillResponse {
144
142
  return extracted;
145
143
  }
146
144
 
147
- function sanitizeSessionToken(input: string): string {
148
- return input.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24) || 'x';
145
+ function extractGatewayCallErrorMessage(value: unknown): string | undefined {
146
+ const objectValue = asObject(value);
147
+ if (!objectValue) return undefined;
148
+
149
+ if (objectValue.ok === false) {
150
+ const rootMessage = typeof objectValue.message === 'string' ? objectValue.message.trim() : '';
151
+ if (rootMessage) return rootMessage;
152
+
153
+ const rootError = asObject(objectValue.error);
154
+ if (rootError) {
155
+ const nestedMessage = typeof rootError.message === 'string' ? rootError.message.trim() : '';
156
+ if (nestedMessage) return nestedMessage;
157
+ }
158
+ return 'gateway returned ok=false';
159
+ }
160
+
161
+ const errorValue = asObject(objectValue.error);
162
+ if (!errorValue) return undefined;
163
+ const message = typeof errorValue.message === 'string' ? errorValue.message.trim() : '';
164
+ return message || 'gateway returned error';
149
165
  }
150
166
 
151
- function nextEphemeralSessionCounter(): number {
152
- ephemeralSessionCounter += 1;
153
- return ephemeralSessionCounter;
167
+ function fillSessionKey(agentId: string): string {
168
+ return `agent:${agentId}:main`;
154
169
  }
155
170
 
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}`;
171
+ async function resetFillSession(
172
+ commandRunner: CommandRunner | null,
173
+ agentId: string,
174
+ timeoutSeconds: number,
175
+ required: boolean
176
+ ): Promise<void> {
177
+ const sessionKey = fillSessionKey(agentId);
178
+ const params = JSON.stringify({
179
+ key: sessionKey,
180
+ reason: 'new'
181
+ });
182
+ const result = await runCommand(
183
+ commandRunner,
184
+ ['openclaw', 'gateway', 'call', 'sessions.reset', '--json', '--params', params],
185
+ {
186
+ timeoutMs: Math.max(10, timeoutSeconds) * 1000
187
+ },
188
+ 'openclaw fill session reset'
189
+ );
190
+
191
+ const fail = (reason: string) => {
192
+ if (!required) return;
193
+ throw new Error(`openclaw fill session reset failed for ${sessionKey}: ${reason}`);
194
+ };
195
+
196
+ if (!result.ok) {
197
+ fail(result.error || result.stderr || result.stdout || `exit=${String(result.code)}`);
198
+ return;
199
+ }
200
+
201
+ const parsed = parseJsonCandidate(result.stdout);
202
+ const gatewayError = parsed !== undefined ? extractGatewayCallErrorMessage(parsed) : undefined;
203
+ if (gatewayError) {
204
+ fail(gatewayError);
205
+ }
164
206
  }
165
207
 
166
208
  class MockFillRunner implements FillRunner {
@@ -213,12 +255,10 @@ class OpenClawFillRunner implements FillRunner {
213
255
  const agentId = (this.config.openclaw_fill_agent_id || 'main').trim() || 'main';
214
256
  const timeoutSeconds = Math.max(10, Math.floor(this.config.session_timeout_seconds));
215
257
  const message = buildOpenClawMessage(context);
216
- const ephemeral = this.config.session_strategy === 'ephemeral';
217
- const sessionId = ephemeral ? buildEphemeralSessionId(agentId, context) : undefined;
218
258
  const argv = ['openclaw', 'agent', '--agent', agentId, '--message', message, '--json', '--timeout', String(timeoutSeconds)];
219
- if (sessionId) {
220
- argv.push('--session-id', sessionId);
221
- }
259
+
260
+ // Always force a fresh fill session via official Gateway session API.
261
+ await resetFillSession(this.commandRunner, agentId, timeoutSeconds, true);
222
262
 
223
263
  try {
224
264
  const output = await runAndReadStdout(
@@ -232,17 +272,7 @@ class OpenClawFillRunner implements FillRunner {
232
272
 
233
273
  return parseFillResponse(output, 'openclaw fill');
234
274
  } 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
- }
275
+ await resetFillSession(this.commandRunner, agentId, timeoutSeconds, false);
246
276
  }
247
277
  }
248
278
  }
package/src/plugin.ts CHANGED
@@ -80,7 +80,6 @@ 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';
84
83
  auto_seed_template?: boolean;
85
84
  data_dir?: string;
86
85
  scheduler_tick_seconds?: number;
@@ -515,10 +514,6 @@ async function runSetup(
515
514
  || asString(resolvedConfig.openclaw_fill_agent_id)
516
515
  || 'main'
517
516
  ).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';
522
517
 
523
518
  if (selectedProvider === 'command' && !selectedCommand) {
524
519
  return {
@@ -592,10 +587,10 @@ async function runSetup(
592
587
  ),
593
588
  fill_provider: selectedProvider,
594
589
  allow_command_fill: selectedAllowCommandFill,
595
- session_strategy: selectedSessionStrategy,
596
590
  auto_seed_template: selectedAutoSeed,
597
591
  display_profile: displayProfile
598
592
  };
593
+ delete nextPluginConfig.session_strategy;
599
594
 
600
595
  if (selectedCommand) {
601
596
  nextPluginConfig.fill_command = selectedCommand;
@@ -636,7 +631,6 @@ async function runSetup(
636
631
  allow_command_fill: selectedAllowCommandFill,
637
632
  fill_command: selectedProvider === 'command' ? selectedCommand : undefined,
638
633
  openclaw_fill_agent_id: selectedProvider === 'openclaw' ? selectedAgentId : undefined,
639
- session_strategy: selectedSessionStrategy,
640
634
  auto_seed_template: selectedAutoSeed,
641
635
  data_dir: nextPluginConfig.data_dir,
642
636
  scheduler_tick_seconds: nextPluginConfig.scheduler_tick_seconds,
@@ -1071,7 +1065,6 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
1071
1065
  allow_command_fill: { type: 'boolean' },
1072
1066
  fill_command: { type: 'string' },
1073
1067
  openclaw_fill_agent_id: { type: 'string' },
1074
- session_strategy: { type: 'string', enum: ['persistent', 'ephemeral'] },
1075
1068
  auto_seed_template: { type: 'boolean' },
1076
1069
  data_dir: { type: 'string' },
1077
1070
  scheduler_tick_seconds: { type: 'number' },
@@ -66,7 +66,6 @@ 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',
70
69
  python_bin: 'python3',
71
70
  writer_script_path: join(process.cwd(), 'scripts', 'dashboard_write.py'),
72
71
  dashboard_output_path: join(root, 'dashboard.json'),
package/src/types.ts CHANGED
@@ -15,8 +15,6 @@ export interface ModelDefaults {
15
15
  max_tokens?: number;
16
16
  }
17
17
 
18
- export type SessionStrategy = 'persistent' | 'ephemeral';
19
-
20
18
  export interface PlashboardConfig {
21
19
  data_dir: string;
22
20
  timezone: string;
@@ -30,7 +28,6 @@ export interface PlashboardConfig {
30
28
  allow_command_fill: boolean;
31
29
  fill_command?: string;
32
30
  openclaw_fill_agent_id?: string;
33
- session_strategy: SessionStrategy;
34
31
  python_bin: string;
35
32
  writer_script_path: string;
36
33
  dashboard_output_path: string;