@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 +6 -0
- package/package.json +1 -1
- package/src/fill-runner.test.ts +122 -18
- package/src/fill-runner.ts +81 -10
- package/src/plugin.ts +1 -0
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
package/src/fill-runner.test.ts
CHANGED
|
@@ -68,20 +68,38 @@ function context(): FillRunContext {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
describe('createFillRunner', () => {
|
|
71
|
-
it('
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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 () => {
|
package/src/fill-runner.ts
CHANGED
|
@@ -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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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