Update workspace agent file (#889)
This commit is contained in:
@@ -14,6 +14,7 @@ import { deleteChannelAccountConfig } from '../../utils/channel-config';
|
|||||||
import { syncAgentModelOverrideToRuntime, syncAllProviderAuthToRuntime } from '../../services/providers/provider-runtime-sync';
|
import { syncAgentModelOverrideToRuntime, syncAllProviderAuthToRuntime } from '../../services/providers/provider-runtime-sync';
|
||||||
import type { HostApiContext } from '../context';
|
import type { HostApiContext } from '../context';
|
||||||
import { parseJsonBody, sendJson } from '../route-utils';
|
import { parseJsonBody, sendJson } from '../route-utils';
|
||||||
|
import { ensureClawXContext } from '../../utils/openclaw-workspace';
|
||||||
|
|
||||||
function scheduleGatewayReload(ctx: HostApiContext, reason: string): void {
|
function scheduleGatewayReload(ctx: HostApiContext, reason: string): void {
|
||||||
if (ctx.gatewayManager.getStatus().state !== 'stopped') {
|
if (ctx.gatewayManager.getStatus().state !== 'stopped') {
|
||||||
@@ -128,6 +129,11 @@ export async function handleAgentRoutes(
|
|||||||
console.warn('[agents] Failed to sync provider auth after agent creation:', err);
|
console.warn('[agents] Failed to sync provider auth after agent creation:', err);
|
||||||
});
|
});
|
||||||
scheduleGatewayReload(ctx, 'create-agent');
|
scheduleGatewayReload(ctx, 'create-agent');
|
||||||
|
// Ensure newly provisioned workspaces get ClawX context merge/cleanup
|
||||||
|
// even when gateway status events do not fire (e.g. in-process reload).
|
||||||
|
void ensureClawXContext().catch((err) => {
|
||||||
|
console.warn('[agents] Failed to ensure ClawX context after agent creation:', err);
|
||||||
|
});
|
||||||
sendJson(res, 200, { success: true, ...snapshot });
|
sendJson(res, 200, { success: true, ...snapshot });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendJson(res, 500, { success: false, error: String(error) });
|
sendJson(res, 500, { success: false, error: String(error) });
|
||||||
|
|||||||
@@ -43,6 +43,66 @@ export function mergeClawXSection(existing: string, section: string): string {
|
|||||||
return existing.trimEnd() + '\n\n' + wrapped + '\n';
|
return existing.trimEnd() + '\n\n' + wrapped + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip the "## First Run" section from workspace AGENTS.md content.
|
||||||
|
* This section is seeded by the OpenClaw Gateway but is unnecessary
|
||||||
|
* for ClawX-managed workspaces. Removes everything from the heading
|
||||||
|
* line until the next markdown heading (any level) or end of content.
|
||||||
|
*/
|
||||||
|
export function stripFirstRunSection(content: string): string {
|
||||||
|
const lines = content.split('\n');
|
||||||
|
const result: string[] = [];
|
||||||
|
let skipping = false;
|
||||||
|
let consumedFirstParagraph = false;
|
||||||
|
let seenBlankAfterParagraph = false;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const isHeading = /^#{1,6}\s/.test(line);
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (line.trim() === '## First Run') {
|
||||||
|
skipping = true;
|
||||||
|
consumedFirstParagraph = false;
|
||||||
|
seenBlankAfterParagraph = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipping) {
|
||||||
|
// A new heading marks the end of the First Run block.
|
||||||
|
if (isHeading) {
|
||||||
|
skipping = false;
|
||||||
|
} else if (!consumedFirstParagraph) {
|
||||||
|
// Drop leading blank lines and the first guidance paragraph.
|
||||||
|
if (trimmed.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
consumedFirstParagraph = true;
|
||||||
|
continue;
|
||||||
|
} else if (!seenBlankAfterParagraph) {
|
||||||
|
// Keep consuming the same paragraph until a blank line appears.
|
||||||
|
if (trimmed.length === 0) {
|
||||||
|
seenBlankAfterParagraph = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// After paragraph + blank line, preserve subsequent body content.
|
||||||
|
if (trimmed.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
skipping = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skipping) {
|
||||||
|
result.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapse any resulting triple+ blank lines into double
|
||||||
|
return result.join('\n').replace(/\n{3,}/g, '\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
// ── Workspace directory resolution ───────────────────────────────
|
// ── Workspace directory resolution ───────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -173,10 +233,22 @@ async function mergeClawXContextOnce(): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const section = await readFile(join(contextDir, file), 'utf-8');
|
const section = await readFile(join(contextDir, file), 'utf-8');
|
||||||
const existing = await readFile(targetPath, 'utf-8');
|
const originalExisting = await readFile(targetPath, 'utf-8');
|
||||||
|
let existing = originalExisting;
|
||||||
|
|
||||||
|
// Strip unwanted Gateway-seeded sections before merging
|
||||||
|
if (targetName === 'AGENTS.md') {
|
||||||
|
const stripped = stripFirstRunSection(existing);
|
||||||
|
if (stripped !== existing) {
|
||||||
|
existing = stripped;
|
||||||
|
logger.info(`Stripped First Run section from ${targetName} (${workspaceDir})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const merged = mergeClawXSection(existing, section);
|
const merged = mergeClawXSection(existing, section);
|
||||||
if (merged !== existing) {
|
// Compare against on-disk content so we persist changes even when only
|
||||||
|
// First Run stripping happened and the ClawX section stayed identical.
|
||||||
|
if (merged !== originalExisting) {
|
||||||
await writeFile(targetPath, merged, 'utf-8');
|
await writeFile(targetPath, merged, 'utf-8');
|
||||||
logger.info(`Merged ClawX context into ${targetName} (${workspaceDir})`);
|
logger.info(`Merged ClawX context into ${targetName} (${workspaceDir})`);
|
||||||
}
|
}
|
||||||
|
|||||||
139
tests/unit/strip-first-run.test.ts
Normal file
139
tests/unit/strip-first-run.test.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { mergeClawXSection, stripFirstRunSection } from '../../electron/utils/openclaw-workspace';
|
||||||
|
|
||||||
|
describe('stripFirstRunSection', () => {
|
||||||
|
it('removes the First Run section when it exists', () => {
|
||||||
|
const input = [
|
||||||
|
'# AGENTS.md',
|
||||||
|
'',
|
||||||
|
'Some preamble content.',
|
||||||
|
'',
|
||||||
|
'## First Run',
|
||||||
|
'',
|
||||||
|
"If `BOOTSTRAP.md` exists, that's your birth certificate. Follow it, figure out who you are, then delete it. You won't need it again.",
|
||||||
|
'',
|
||||||
|
'## Other Section',
|
||||||
|
'',
|
||||||
|
'Other content.',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const result = stripFirstRunSection(input);
|
||||||
|
expect(result).not.toContain('## First Run');
|
||||||
|
expect(result).not.toContain('BOOTSTRAP.md');
|
||||||
|
expect(result).toContain('# AGENTS.md');
|
||||||
|
expect(result).toContain('Some preamble content.');
|
||||||
|
expect(result).toContain('## Other Section');
|
||||||
|
expect(result).toContain('Other content.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns content unchanged when no First Run section exists', () => {
|
||||||
|
const input = '# AGENTS.md\n\nSome content.\n';
|
||||||
|
expect(stripFirstRunSection(input)).toBe(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles First Run section at end of file', () => {
|
||||||
|
const input = [
|
||||||
|
'# AGENTS.md',
|
||||||
|
'',
|
||||||
|
'## First Run',
|
||||||
|
'',
|
||||||
|
'Bootstrap text.',
|
||||||
|
'',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const result = stripFirstRunSection(input);
|
||||||
|
expect(result).not.toContain('## First Run');
|
||||||
|
expect(result).not.toContain('Bootstrap text');
|
||||||
|
expect(result).toContain('# AGENTS.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not collapse adjacent sections', () => {
|
||||||
|
const input = [
|
||||||
|
'## Section A',
|
||||||
|
'content a',
|
||||||
|
'',
|
||||||
|
'## First Run',
|
||||||
|
'',
|
||||||
|
'bootstrap text',
|
||||||
|
'',
|
||||||
|
'## Section B',
|
||||||
|
'content b',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const result = stripFirstRunSection(input);
|
||||||
|
expect(result).toContain('## Section A');
|
||||||
|
expect(result).toContain('content a');
|
||||||
|
expect(result).toContain('## Section B');
|
||||||
|
expect(result).toContain('content b');
|
||||||
|
expect(result).not.toContain('## First Run');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not remove sections with similar but different names', () => {
|
||||||
|
const input = [
|
||||||
|
'## First Run Setup',
|
||||||
|
'This should stay.',
|
||||||
|
'',
|
||||||
|
'## First Run',
|
||||||
|
'This should go.',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const result = stripFirstRunSection(input);
|
||||||
|
expect(result).toContain('## First Run Setup');
|
||||||
|
expect(result).toContain('This should stay.');
|
||||||
|
expect(result).not.toContain('This should go.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('collapses triple blank lines left by removal', () => {
|
||||||
|
const input = [
|
||||||
|
'before',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'## First Run',
|
||||||
|
'',
|
||||||
|
'text',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'after',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const result = stripFirstRunSection(input);
|
||||||
|
expect(result).not.toMatch(/\n{3,}/);
|
||||||
|
expect(result).toContain('before');
|
||||||
|
expect(result).toContain('after');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still changes AGENTS content when only First Run is removed', () => {
|
||||||
|
const section = [
|
||||||
|
'## ClawX Environment',
|
||||||
|
'',
|
||||||
|
'You are ClawX.',
|
||||||
|
].join('\n');
|
||||||
|
const original = [
|
||||||
|
'# AGENTS.md',
|
||||||
|
'',
|
||||||
|
'## First Run',
|
||||||
|
'',
|
||||||
|
"If `BOOTSTRAP.md` exists, that's your birth certificate. Follow it, figure out who you are, then delete it. You won't need it again.",
|
||||||
|
'',
|
||||||
|
'## Session Startup',
|
||||||
|
'',
|
||||||
|
'Read SOUL.md first.',
|
||||||
|
'',
|
||||||
|
'<!-- clawx:begin -->',
|
||||||
|
'## ClawX Environment',
|
||||||
|
'',
|
||||||
|
'You are ClawX.',
|
||||||
|
'<!-- clawx:end -->',
|
||||||
|
'',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const stripped = stripFirstRunSection(original);
|
||||||
|
const merged = mergeClawXSection(stripped, section);
|
||||||
|
|
||||||
|
expect(merged).not.toBe(original);
|
||||||
|
expect(merged).not.toContain('## First Run');
|
||||||
|
expect(merged).toContain('## Session Startup');
|
||||||
|
expect(merged).toContain('<!-- clawx:begin -->');
|
||||||
|
expect(merged).toContain('<!-- clawx:end -->');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user