@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 +29 -0
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
- package/src/config.ts +5 -0
- package/src/fill-runner.test.ts +104 -1
- package/src/fill-runner.ts +51 -10
- package/src/plugin.ts +8 -0
- package/src/runtime.test.ts +1 -0
- package/src/types.ts +3 -0
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
|
package/openclaw.plugin.json
CHANGED
|
@@ -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
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,
|
package/src/fill-runner.test.ts
CHANGED
|
@@ -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('
|
|
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 () => {
|
package/src/fill-runner.ts
CHANGED
|
@@ -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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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' },
|
package/src/runtime.test.ts
CHANGED
|
@@ -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;
|