@jhytabest/plashboard 1.0.0 → 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
@@ -77,6 +77,12 @@ Add to `openclaw.json`:
77
77
  `fill_provider: "command"` requires explicit opt-in with `allow_command_fill: true`.
78
78
  Use command mode only if you need a custom external runner.
79
79
 
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.
83
+
84
+ This keeps fill runs stateless without editing OpenClaw session files directly.
85
+
80
86
  For production stability, use a dedicated fill agent instead of `main`:
81
87
 
82
88
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhytabest/plashboard",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "private": false,
5
5
  "description": "Plashboard OpenClaw plugin runtime",
6
6
  "license": "MIT",
@@ -68,20 +68,38 @@ function context(): FillRunContext {
68
68
  }
69
69
 
70
70
  describe('createFillRunner', () => {
71
- it('parses openclaw json envelope output', async () => {
72
- const commandRunner = vi.fn(async (_argv: string[], _options: unknown) => ({
73
- stdout: JSON.stringify({
74
- result: {
75
- payloads: [
76
- {
77
- 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
+ ]
78
91
  }
79
- ]
80
- }
81
- }),
82
- stderr: '',
83
- code: 0
84
- }));
92
+ }),
93
+ stderr: '',
94
+ code: 0
95
+ };
96
+ }
97
+ return {
98
+ stdout: '',
99
+ stderr: `unsupported command: ${argv.join(' ')}`,
100
+ code: 1
101
+ };
102
+ });
85
103
 
86
104
  const runner = createFillRunner(
87
105
  config({ fill_provider: 'openclaw', openclaw_fill_agent_id: 'ops' }),
@@ -90,12 +108,98 @@ describe('createFillRunner', () => {
90
108
  const response = await runner.run(context());
91
109
 
92
110
  expect(response.values.summary).toBe('new summary');
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
+ }
129
+ });
130
+
131
+ it('fails fill when pre-run session reset fails', async () => {
132
+ const commandRunner = vi.fn(async (argv: string[], _options: unknown) => {
133
+ if (argv[0] === 'openclaw' && argv[1] === 'gateway' && argv[2] === 'call' && argv[3] === 'sessions.reset') {
134
+ return {
135
+ stdout: '',
136
+ stderr: 'reset failed',
137
+ code: 1
138
+ };
139
+ }
140
+ return {
141
+ stdout: '',
142
+ stderr: `unsupported command: ${argv.join(' ')}`,
143
+ code: 1
144
+ };
145
+ });
146
+
147
+ const runner = createFillRunner(
148
+ config({
149
+ fill_provider: 'openclaw',
150
+ openclaw_fill_agent_id: 'ops'
151
+ }),
152
+ { commandRunner }
153
+ );
154
+
155
+ await expect(runner.run(context())).rejects.toThrow(/session reset failed/i);
93
156
  expect(commandRunner).toHaveBeenCalledTimes(1);
94
- const firstCall = commandRunner.mock.calls[0];
95
- const argv = firstCall[0];
96
- expect(argv.slice(0, 2)).toEqual(['openclaw', 'agent']);
97
- expect(argv).toContain('--agent');
98
- expect(argv).toContain('ops');
157
+ });
158
+
159
+ it('post-run session reset failure is safe and does not fail fill output', async () => {
160
+ let resetCalls = 0;
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
+ }
177
+ if (argv[0] === 'openclaw' && argv[1] === 'agent') {
178
+ return {
179
+ stdout: '{"values":{"summary":"new summary"}}',
180
+ stderr: '',
181
+ code: 0
182
+ };
183
+ }
184
+ return {
185
+ stdout: '',
186
+ stderr: `unsupported command: ${argv.join(' ')}`,
187
+ code: 1
188
+ };
189
+ });
190
+
191
+ const runner = createFillRunner(
192
+ config({
193
+ fill_provider: 'openclaw',
194
+ openclaw_fill_agent_id: 'ops'
195
+ }),
196
+ { commandRunner }
197
+ );
198
+ const response = await runner.run(context());
199
+
200
+ expect(response.values.summary).toBe('new summary');
201
+ expect(commandRunner).toHaveBeenCalledTimes(3);
202
+ expect(commandRunner.mock.calls[2][0].slice(0, 4)).toEqual(['openclaw', 'gateway', 'call', 'sessions.reset']);
99
203
  });
100
204
 
101
205
  it('parses command runner fenced json output', async () => {
@@ -1,4 +1,4 @@
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 {
@@ -142,6 +142,69 @@ function parseFillResponse(output: string, source: string): FillResponse {
142
142
  return extracted;
143
143
  }
144
144
 
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';
165
+ }
166
+
167
+ function fillSessionKey(agentId: string): string {
168
+ return `agent:${agentId}:main`;
169
+ }
170
+
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
+ }
206
+ }
207
+
145
208
  class MockFillRunner implements FillRunner {
146
209
  async run(context: FillRunContext): Promise<FillResponse> {
147
210
  const values: Record<string, unknown> = {};
@@ -192,17 +255,25 @@ class OpenClawFillRunner implements FillRunner {
192
255
  const agentId = (this.config.openclaw_fill_agent_id || 'main').trim() || 'main';
193
256
  const timeoutSeconds = Math.max(10, Math.floor(this.config.session_timeout_seconds));
194
257
  const message = buildOpenClawMessage(context);
258
+ const argv = ['openclaw', 'agent', '--agent', agentId, '--message', message, '--json', '--timeout', String(timeoutSeconds)];
195
259
 
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
- );
260
+ // Always force a fresh fill session via official Gateway session API.
261
+ await resetFillSession(this.commandRunner, agentId, timeoutSeconds, true);
262
+
263
+ try {
264
+ const output = await runAndReadStdout(
265
+ this.commandRunner,
266
+ argv,
267
+ {
268
+ timeoutMs: (timeoutSeconds + 30) * 1000
269
+ },
270
+ 'openclaw fill'
271
+ );
204
272
 
205
- return parseFillResponse(output, 'openclaw fill');
273
+ return parseFillResponse(output, 'openclaw fill');
274
+ } finally {
275
+ await resetFillSession(this.commandRunner, agentId, timeoutSeconds, false);
276
+ }
206
277
  }
207
278
  }
208
279
 
package/src/plugin.ts CHANGED
@@ -590,6 +590,7 @@ async function runSetup(
590
590
  auto_seed_template: selectedAutoSeed,
591
591
  display_profile: displayProfile
592
592
  };
593
+ delete nextPluginConfig.session_strategy;
593
594
 
594
595
  if (selectedCommand) {
595
596
  nextPluginConfig.fill_command = selectedCommand;