mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
fix: pre-landing review fixes (GST-110)
Entries point at a full SKILL.md in a plain copy of every skill (the router's gstack/skills/ in hybrid, .gstack-skills/ under --skills all), so autoplan's ../gstack-plan-eng-review/SKILL.md and other sibling paths reach full texts; test the #3555 upgrade path. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
a82cec9564
commit
d4a682dd48
5 files changed
+48
-27
No files matched your search
+2
-2
@@ -5,11 +5,11 @@
|
||||
**OpenClaw opens gstack's large task skills again (GST-110).** #3555 made `hybrid` OpenClaw's default and registered review, investigate, qa, ship and plan-eng-review as skills of their own. OpenClaw 2026.10.6 serves a registered skill's `SKILL.md` whole or refuses it, with a 32,768-byte limit when its model catalog has no context window for the model. In HARN-5's panel the agent's first read of the 77,691-byte `gstack-review` file was refused in 4 of 5 trials, and opens fell from 5/5 to 0/5. Under the router layout the same file had been read as a plain file inside the router's folder, which OpenClaw pages.
|
||||
|
||||
### Fixed
|
||||
- **`gstack-harness register openclaw`** (`lib/harness-register.ts`): a skill whose rendered `SKILL.md` is over 32,768 bytes (`OPENCLAW_INSTRUCTION_MAX`, with the OpenClaw source lines it comes from) is registered as a short entry with the same frontmatter whose body says to read `SKILL.full.md` in the same folder in full and follow it. The full text stays beside its `sections/` and sibling skills, so its relative paths resolve as before. That covers four of hybrid's five direct skills and 26 of 63 skills in `--skills all` today. OpenClaw treats only a skill folder's `SKILL.md` as an instruction file. The ledger records `SKILL.full.md`, so refresh, mode switches and uninstall handle it. Skill bodies are unchanged, and Hermes, dots and `generic` registrations are byte-identical.
|
||||
- **`gstack-harness register openclaw`** (`lib/harness-register.ts`): a skill whose rendered `SKILL.md` is over 32,768 bytes (`OPENCLAW_INSTRUCTION_MAX`, with the OpenClaw source lines it comes from) is registered as a short entry with the same frontmatter whose body says to read the skill's full `SKILL.md` in a plain copy of every skill and follow it. In hybrid that copy is the router's `gstack/skills/`, where the router layout read the same file, now including the five task skills; under `--skills all` it is `.gstack-skills/`, a folder OpenClaw's skill scan skips. Because the copy holds every skill, `sections/` paths and sibling paths such as `../gstack-qa/` and autoplan's `../gstack-plan-eng-review/SKILL.md` resolve to full texts. That covers four of hybrid's five direct skills and 26 of 63 skills in `--skills all` today. OpenClaw treats only a registered skill folder's `SKILL.md` as an instruction file. The ledger records the copies, so refresh, mode switches, upgrades from a #3555 registration and uninstall handle them. Skill bodies are unchanged, and Hermes, dots and `generic` registrations are byte-identical.
|
||||
- docs/hosts.md and docs/OPENCLAW.md describe the entry and the limit.
|
||||
|
||||
### Tests
|
||||
- `test/harness-register.test.ts`: in hybrid and all, every `SKILL.md` OpenClaw would register is within the limit, each entry names a `SKILL.full.md` that exists, holds the full text with the same frontmatter, is outside OpenClaw's registered set and is in the ledger, and uninstall leaves nothing. Hermes's registration equals its render byte for byte. With `GSTACK_OPENCLAW_BIN` or `openclaw` on PATH, a real `openclaw skills list --json` and `skills info` show exactly `gstack` and the five task skills, each served from an entry within the limit.
|
||||
- `test/harness-register.test.ts`: in hybrid and all, every `SKILL.md` OpenClaw would register is within the limit, each entry names a full `SKILL.md` that exists, has the same frontmatter, is outside OpenClaw's registered set and is in the ledger, every `../gstack-*` path in the copied texts resolves to a full text, re-registering over a #3555 registration converts it cleanly, and uninstall leaves nothing. Hermes's registration equals its render byte for byte. With `GSTACK_OPENCLAW_BIN` or `openclaw` on PATH, a real `openclaw skills list --json` and `skills info` show exactly `gstack` and the five task skills, each served from an entry within the limit.
|
||||
|
||||
## [1.91.319.0] - 2026-10-29
|
||||
|
||||
|
||||
+3
-2
@@ -191,8 +191,9 @@ skills when the OpenClaw agent should do it. Both can be installed together.
|
||||
OpenClaw serves a registered skill's `SKILL.md` whole or refuses it, and its
|
||||
limit is 32,768 bytes when it does not know the model's context window, so a
|
||||
larger skill (review, investigate, ship and plan-eng-review among the five) is
|
||||
registered as a short entry that tells the agent to read `SKILL.full.md` beside
|
||||
it, a plain file OpenClaw reads in pages.
|
||||
registered as a short entry that tells the agent to read the skill's full
|
||||
`SKILL.md` in `gstack/skills/`, a plain copy of every skill that OpenClaw
|
||||
reads in pages (`.gstack-skills/` under `--skills all`).
|
||||
Install, verify and remove: [hosts.md](hosts.md#agent-harnesses).
|
||||
|
||||
The ACP artifacts in `openclaw/` are generated by `bun run gen:skill-docs --host openclaw`.
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ characters, the part of a description Hermes's skills index shows.
|
||||
|
||||
| Harness | What gstack registers | Install | Verify | Remove |
|
||||
|---|---|---|---|---|
|
||||
| OpenClaw | default `--skills hybrid`: `gstack-review`, `gstack-investigate`, `gstack-qa`, `gstack-ship` and `gstack-plan-eng-review` as top-level skills plus one `gstack` router skill whose body lists every other skill and its path, in `${OPENCLAW_STATE_DIR:-~/.openclaw}/skills`. With OpenClaw 2026.10.6's 19 eligible bundled skills, hybrid's skills section is 10,290 characters, full format, under its 18,000-character budget; `--skills all` (17,971, compact format, descriptions cut) lists every skill as its own folder, `--skills router` (6,701) only the router. OpenClaw serves a registered skill's `SKILL.md` whole or refuses it ("Skill instructions cannot be partially served"), at most 32,768 bytes when its model catalog has no context window for the model, so any registered skill larger than that (four of the five in hybrid, 26 of 63 in `all` today) is a short entry `SKILL.md` with the same name and description that sends the agent to its full text in `SKILL.full.md` beside it, which OpenClaw reads as a plain file | `./setup --host openclaw`, or `./setup` inside OpenClaw | `openclaw skills list --json` shows `gstack` and the five task skills (source `openclaw-managed`, eligible) | `gstack-harness unregister openclaw` |
|
||||
| OpenClaw | default `--skills hybrid`: `gstack-review`, `gstack-investigate`, `gstack-qa`, `gstack-ship` and `gstack-plan-eng-review` as top-level skills plus one `gstack` router skill whose body lists every other skill and its path, in `${OPENCLAW_STATE_DIR:-~/.openclaw}/skills`. With OpenClaw 2026.10.6's 19 eligible bundled skills, hybrid's skills section is 10,290 characters, full format, under its 18,000-character budget; `--skills all` (17,971, compact format, descriptions cut) lists every skill as its own folder, `--skills router` (6,701) only the router. OpenClaw serves a registered skill's `SKILL.md` whole or refuses it ("Skill instructions cannot be partially served"), at most 32,768 bytes when its model catalog has no context window for the model, so any registered skill larger than that (four of the five in hybrid, 26 of 63 in `all` today) is a short entry `SKILL.md` with the same name and description that sends the agent to its full `SKILL.md` in a plain copy of every skill (the router's `gstack/skills/`, or `.gstack-skills/` under `--skills all`, a folder OpenClaw's skill scan skips), which OpenClaw reads in pages. With a known context window OpenClaw also caps a tool result (16,000 characters under 100K tokens), which this limit does not cover | `./setup --host openclaw`, or `./setup` inside OpenClaw | `openclaw skills list --json` shows `gstack` and the five task skills (source `openclaw-managed`, eligible) | `gstack-harness unregister openclaw` |
|
||||
| Hermes | every skill, as `gstack-<skill>` in one `gstack` category, in `${HERMES_HOME:-~/.hermes}/skills/gstack`. Hermes's skills index lists only skills registered as folders, so a router would leave the rest out of the listing (`skill_view` still loads an unlisted skill by name, but nothing tells the model it exists) | `./setup --host hermes`, or `./setup` inside Hermes | `hermes skills list` shows the `gstack-*` skills in category `gstack` | `gstack-harness unregister hermes` |
|
||||
| dots (feder-cr) | the `gstack` router in `/home/dot/skills/gstack`, which the Dot lists every turn | `./setup` on the Dot's computer, or `gstack-harness register dots` | the Dot's Skills page, or `GET /skills` | `gstack-harness unregister dots` |
|
||||
| QM | **not available yet.** QM imports a skill pack by cloning the whole repository and caps it at 32 MiB and 5,000 files; gstack's repository is about 73 MiB, so `gstack-harness register qm --yes` stops before sending anything ([why](troubleshooting.md#harness-qm-pack-too-large)) | meanwhile, paste `gstack-harness register generic --print` into the agent's instructions | ask it for a gstack skill | delete the pasted text |
|
||||
|
||||
+16
-15
@@ -40,12 +40,14 @@ export const HYBRID_SKILLS = ['review', 'investigate', 'qa', 'ship', 'plan-eng-r
|
||||
/**
|
||||
* OpenClaw serves a registered skill's SKILL.md whole or refuses it ("Skill instructions cannot be partially
|
||||
* served"), with maxBytes from resolveAdaptiveReadMaxBytes: 32,768 (DEFAULT_READ_PAGE_MAX_BYTES) when the model's
|
||||
* context window is unknown, never less (OpenClaw 2026.10.6 dist/agent-tools.read-BIxUGJy-.mjs:168, :189-194, :747,
|
||||
* :798, :819-821). A larger skill is registered as a short entry that points at its full text in OPENCLAW_FULL_SKILL.
|
||||
* context window is unknown (OpenClaw 2026.10.6 dist/agent-tools.read-BIxUGJy-.mjs:168, :189-194, :747, :798,
|
||||
* :819-821). A known window adds a live tool-result cap that can be lower (16,000 characters under 100K tokens,
|
||||
* dist/tool-result-limits-DNBSu_jg.mjs:61-79); this limit does not cover it. A larger skill is registered as a short
|
||||
* entry that points at its full SKILL.md in a plain tree of every skill, which OpenClaw reads in pages.
|
||||
*/
|
||||
export const OPENCLAW_INSTRUCTION_MAX = 32_768;
|
||||
/** Beside the entry, so the full text's relative paths (sections/, ../gstack-qa/) resolve as before; only a skill folder's SKILL.md is an instruction file (dist/skill-root-discovery-C2QVZC77.mjs:343). */
|
||||
export const OPENCLAW_FULL_SKILL = 'SKILL.full.md';
|
||||
/** The plain tree under --skills all: a dot folder, which OpenClaw's skill scan skips (dist/skill-root-discovery-C2QVZC77.mjs:44). Hybrid and router use the router's own gstack/skills, which the scan never enters because gstack/ holds a SKILL.md (:343). */
|
||||
export const OPENCLAW_ALL_TEXTS = '.gstack-skills';
|
||||
|
||||
export interface Target {
|
||||
name: TargetName;
|
||||
@@ -256,9 +258,9 @@ export function entrySkill(md: string, fullPath: string, target: Target): string
|
||||
return [
|
||||
md.slice(0, md.indexOf('\n', end + 4) + 1) + '<!-- AUTO-GENERATED by gstack-harness register: rerun it instead of editing -->',
|
||||
'# ' + frontmatterField(md, 'name'), '',
|
||||
'This skill\'s full instructions (' + Buffer.byteLength(md) + ' bytes) are larger than ' + target.displayName + ' serves as a skill file, so they are in a plain file beside this one.'
|
||||
'This skill\'s full instructions (' + Buffer.byteLength(md) + ' bytes) are larger than ' + target.displayName + ' serves as a skill file, so they are in a plain file.'
|
||||
+ ' Read `' + fullPath + '` in full now (every page, if the read is split) and follow it as this skill\'s instructions.'
|
||||
+ ' Paths it gives relative to the installed SKILL.md directory resolve from this folder.',
|
||||
+ ' Paths it gives relative to the installed SKILL.md directory resolve from that file\'s folder.',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
@@ -385,27 +387,26 @@ export function planPayload(opts: RegisterOptions, dest: string, mode: SkillsMod
|
||||
files.set(path.join(to, 'SKILL.md'), Buffer.from(flatFrontmatter(fs.readFileSync(path.join(s.dir, 'SKILL.md'), 'utf8'), path.basename(to))));
|
||||
if (fs.existsSync(path.join(s.dir, 'sections'))) copyDir(path.join(s.dir, 'sections'), path.join(to, 'sections'));
|
||||
};
|
||||
const routerDir = path.join(dest, 'gstack');
|
||||
const plain = path.join(...(mode === 'all' ? [dest, OPENCLAW_ALL_TEXTS] : [routerDir, 'skills']));
|
||||
const listed = (s: SkillEntry) => {
|
||||
const to = path.join(dest, path.basename(s.dir));
|
||||
if (t.render === 'host') copyDir(s.dir, to); else claudeFiles(s, to);
|
||||
const md = path.join(to, 'SKILL.md');
|
||||
const full = files.get(md)!;
|
||||
if (t.instructionMax && full.length > t.instructionMax) {
|
||||
files.set(path.join(to, OPENCLAW_FULL_SKILL), full);
|
||||
files.set(md, Buffer.from(entrySkill(full.toString('utf8'), path.join(to, OPENCLAW_FULL_SKILL), t)));
|
||||
}
|
||||
const md = fs.readFileSync(path.join(s.dir, 'SKILL.md'));
|
||||
if (t.instructionMax && md.length > t.instructionMax) files.set(path.join(to, 'SKILL.md'), Buffer.from(entrySkill(md.toString('utf8'), path.join(plain, path.basename(s.dir), 'SKILL.md'), t)));
|
||||
else if (t.render === 'host') copyDir(s.dir, to); else claudeFiles(s, to);
|
||||
return t.render === 'host' ? s.name : path.basename(s.dir);
|
||||
};
|
||||
if (mode === 'all') {
|
||||
const names = skills.map(listed);
|
||||
if (t.instructionMax) for (const s of skills) copyDir(s.dir, path.join(plain, path.basename(s.dir)));
|
||||
return { files, router: null, visible: names, direct: [], skills: names.length };
|
||||
}
|
||||
const isDirect = (s: SkillEntry) => mode === 'hybrid' && HYBRID_SKILLS.includes(path.basename(s.dir).replace(/^gstack-/, ''));
|
||||
const direct = skills.filter(isDirect).map(listed);
|
||||
const routerDir = path.join(dest, 'gstack');
|
||||
if (t.instructionMax) for (const s of skills.filter(isDirect)) copyDir(s.dir, path.join(plain, path.basename(s.dir)));
|
||||
const entries = skills.filter(s => !isDirect(s)).map(s => {
|
||||
if (t.render !== 'host') return { name: s.name, use: s.use, path: path.join(s.dir, 'SKILL.md') };
|
||||
const to = path.join(routerDir, 'skills', path.basename(s.dir));
|
||||
const to = path.join(plain, path.basename(s.dir));
|
||||
copyDir(s.dir, to);
|
||||
return { name: s.name, use: s.use, path: path.join(to, 'SKILL.md') };
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import {
|
||||
BLOCK_BEGIN, BLOCK_END, CONTEXT_BLOCK_MAX, DOT_SKILLS_DIR, HARNESS_CODES, HYBRID_SKILLS, HarnessError, OPENCLAW_FULL_SKILL, OPENCLAW_INSTRUCTION_MAX, ROUTER_DESCRIPTION, ROUTER_DESCRIPTION_MAX, TARGETS,
|
||||
BLOCK_BEGIN, BLOCK_END, CONTEXT_BLOCK_MAX, DOT_SKILLS_DIR, HARNESS_CODES, HYBRID_SKILLS, HarnessError, OPENCLAW_ALL_TEXTS, OPENCLAW_INSTRUCTION_MAX, ROUTER_DESCRIPTION, ROUTER_DESCRIPTION_MAX, TARGETS,
|
||||
contextBlock, contextScope, detect, listLedgers, readLedger, refresh, register, sourceSkills, unregister, type RegisterOptions,
|
||||
} from '../lib/harness-register';
|
||||
import { QM_PACK_LIMITS, qmRequest, registerQm, removeQmPack } from '../lib/harness-qm';
|
||||
@@ -89,7 +89,7 @@ describe('gstack-harness register and unregister (skill folders)', () => {
|
||||
test('OpenClaw all: every skill a top-level folder with a unique name', () => {
|
||||
const d = tmp();
|
||||
const r = register(opts(d, 'openclaw', { mode: 'all', skillsDir: path.join(d, 'skills') }));
|
||||
const names = fs.readdirSync(path.join(d, 'skills')).map(n => fs.readFileSync(path.join(d, 'skills', n, 'SKILL.md'), 'utf8').match(/^name:\s*(\S+)/m)![1]);
|
||||
const names = fs.readdirSync(path.join(d, 'skills')).filter(n => !n.startsWith('.')).map(n => fs.readFileSync(path.join(d, 'skills', n, 'SKILL.md'), 'utf8').match(/^name:\s*(\S+)/m)![1]);
|
||||
expect(names.length).toBe(r.skills);
|
||||
expect(new Set(names).size).toBe(names.length);
|
||||
expect(readLedger(r.ledger)!.meta.names.split(',').sort()).toEqual([...names].sort());
|
||||
@@ -269,7 +269,7 @@ describe('GSTA-135: registered skills get used', () => {
|
||||
for (const mode of ['all', 'router', 'hybrid'] as const) {
|
||||
expect(register(opts(d, 'openclaw', { env, mode })).status).toBe('updated');
|
||||
expect(refresh(base).map(x => [x.mode, x.status])).toEqual([[mode, 'unchanged']]);
|
||||
const top = fs.readdirSync(skills).filter(n => n !== 'mine');
|
||||
const top = fs.readdirSync(skills).filter(n => n !== 'mine' && !n.startsWith('.'));
|
||||
expect(top.length).toBe(mode === 'all' ? r.skills : mode === 'router' ? 1 : 1 + HYBRID_SKILLS.length);
|
||||
}
|
||||
unregister(path.join(d, 'state'), ROOT, r.id);
|
||||
@@ -393,14 +393,15 @@ describe('GST-110: OpenClaw serves every registered skill file whole', () => {
|
||||
.flatMap(e => { const sub = path.join(dir, e.name); return fs.existsSync(path.join(sub, 'SKILL.md')) ? [path.join(sub, 'SKILL.md')] : registered(sub, depth + 1); });
|
||||
const frontmatter = (md: string) => md.slice(0, md.indexOf('\n---', 4) + 4);
|
||||
|
||||
test('hybrid and all: every registered SKILL.md is within the limit; a larger skill is an entry naming its full text, unregistered, beside it; uninstall removes both', () => {
|
||||
test('hybrid and all: every registered SKILL.md is within the limit; a larger skill is an entry naming its full SKILL.md in an unregistered tree of every skill, whose sibling paths resolve; uninstall removes both', () => {
|
||||
const d = tmp();
|
||||
for (const mode of ['hybrid', 'all'] as const) {
|
||||
const skills = path.join(d, mode, 'skills');
|
||||
const plain = mode === 'all' ? path.join(skills, OPENCLAW_ALL_TEXTS) : path.join(skills, 'gstack', 'skills');
|
||||
const r = register(opts(d, 'openclaw', { mode, env: { OPENCLAW_STATE_DIR: path.join(d, mode) }, stateRoot: path.join(d, mode, 'state') }));
|
||||
const mds = registered(skills);
|
||||
expect(mds.length, mode).toBe(mode === 'all' ? r.skills : 1 + HYBRID_SKILLS.length);
|
||||
const entries = mds.filter(md => fs.readFileSync(md, 'utf8').includes(OPENCLAW_FULL_SKILL));
|
||||
const entries = mds.filter(md => fs.readFileSync(md, 'utf8').includes(' in full now (every page'));
|
||||
if (mode === 'hybrid') expect(entries.map(md => path.basename(path.dirname(md)))).toContain('gstack-review');
|
||||
expect(entries.length, mode).toBeGreaterThan(0);
|
||||
for (const md of mds) expect(fs.statSync(md).size, md).toBeLessThanOrEqual(OPENCLAW_INSTRUCTION_MAX);
|
||||
@@ -408,18 +409,36 @@ describe('GST-110: OpenClaw serves every registered skill file whole', () => {
|
||||
for (const md of entries) {
|
||||
const entry = fs.readFileSync(md, 'utf8');
|
||||
const full = /Read \x60([^\x60]+)\x60 in full/.exec(entry)![1];
|
||||
expect(full).toBe(path.join(path.dirname(md), OPENCLAW_FULL_SKILL));
|
||||
expect(full).toBe(path.join(plain, path.basename(path.dirname(md)), 'SKILL.md'));
|
||||
expect(mds).not.toContain(full);
|
||||
expect(files(path.dirname(md))).toEqual(['SKILL.md']);
|
||||
const text = fs.readFileSync(full, 'utf8');
|
||||
expect(Buffer.byteLength(text)).toBeGreaterThan(OPENCLAW_INSTRUCTION_MAX);
|
||||
expect(frontmatter(entry)).toBe(frontmatter(text));
|
||||
expect(ledger).toContain(full);
|
||||
}
|
||||
for (const f of files(plain).filter(f => f.endsWith('.md'))) {
|
||||
for (const [, ref] of fs.readFileSync(path.join(plain, f), 'utf8').matchAll(/\x60\.\.\/(gstack-[a-z-]+\/[^\x60\s]+)\x60/g)) {
|
||||
expect(fs.existsSync(path.join(plain, ref)), f + ' -> ' + ref).toBe(true);
|
||||
expect(fs.readFileSync(path.join(plain, ref), 'utf8'), ref).not.toContain(' in full now (every page');
|
||||
}
|
||||
}
|
||||
unregister(path.join(d, mode, 'state'), ROOT, r.id);
|
||||
expect(files(skills), mode).toEqual([]);
|
||||
}
|
||||
}, 180_000);
|
||||
|
||||
test('re-registering over a #3555 registration turns each large skill into an entry, removes its old files, and keeps nothing as an edit', () => {
|
||||
const d = tmp();
|
||||
const env = { OPENCLAW_STATE_DIR: path.join(d, 'oc') };
|
||||
const old = register(opts(d, 'openclaw', { env, target: { ...TARGETS.openclaw, instructionMax: undefined } }));
|
||||
expect(fs.statSync(path.join(old.dest, 'gstack-review', 'SKILL.md')).size).toBeGreaterThan(OPENCLAW_INSTRUCTION_MAX);
|
||||
const r = register(opts(d, 'openclaw', { env }));
|
||||
expect([r.id, r.status, r.kept]).toEqual([old.id, 'updated', []]);
|
||||
expect(files(path.join(r.dest, 'gstack-review'))).toEqual(['SKILL.md']);
|
||||
for (const md of registered(r.dest)) expect(fs.statSync(md).size, md).toBeLessThanOrEqual(OPENCLAW_INSTRUCTION_MAX);
|
||||
}, 120_000);
|
||||
|
||||
test('Hermes is unchanged: its registration is the Hermes render, byte for byte, with no entry files', () => {
|
||||
const d = tmp();
|
||||
const r = register(opts(d, 'hermes', { env: { HERMES_HOME: path.join(d, 'hermes') } }));
|
||||
@@ -448,7 +467,7 @@ describe('GST-110: OpenClaw serves every registered skill file whole', () => {
|
||||
expect(file.startsWith(r.dest + path.sep) && path.basename(file) === 'SKILL.md', file).toBe(true);
|
||||
expect(fs.statSync(file).size, file).toBeLessThanOrEqual(OPENCLAW_INSTRUCTION_MAX);
|
||||
}
|
||||
expect(fs.existsSync(path.join(r.dest, 'gstack-review', OPENCLAW_FULL_SKILL))).toBe(true);
|
||||
expect(fs.statSync(path.join(r.dest, 'gstack', 'skills', 'gstack-review', 'SKILL.md')).size).toBeGreaterThan(OPENCLAW_INSTRUCTION_MAX);
|
||||
}, 180_000);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user