Get started

بازآرایی چرخهٔ حیات ACP

چرخه عمر ACP در حال حاضر کار می‌کند، اما بخش زیادی از آن پس از وقوع، استنباط می‌شود. پاک‌سازی فرایند مالکیت را از PIDها، رشته‌های فرمان، مسیرهای wrapper و جدول زنده فرایند بازسازی می‌کند. نمایانی نشست مالکیت را از رشته‌های کلید نشست به‌علاوه جست‌وجوهای ثانویه sessions.list({ spawnedBy }) بازسازی می‌کند. این کار اصلاح‌های محدود را ممکن می‌کند، اما باعث می‌شود موارد مرزی نیز به‌راحتی نادیده گرفته شوند: استفاده دوباره از PID، فرمان‌های نقل‌قول‌شده، نوه‌های adapter، ریشه‌های وضعیت چند Gateway، cancel در برابر close، و نمایانی tree در برابر all همگی به جاهای جداگانه‌ای برای کشف دوباره همان قواعد مالکیت تبدیل می‌شوند.

این بازآرایی مالکیت را به مفهومی درجه‌اول تبدیل می‌کند. هدف یک سطح محصول جدید ACP نیست؛ هدف، یک قرارداد داخلی امن‌تر برای رفتار موجود ACP و ACPX است.

اهداف

  • پاک‌سازی هرگز به یک فرایند سیگنال نمی‌دهد مگر اینکه شواهد زنده فعلی با یک اجاره متعلق به OpenClaw مطابقت داشته باشد.
  • cancel، close و جمع‌آوری هنگام راه‌اندازی نیت‌های چرخه عمر متمایزی دارند.
  • sessions_list، sessions_history، sessions_send و بررسی‌های وضعیت از همان مدل نشست متعلق به درخواست‌کننده استفاده می‌کنند.
  • نصب‌های چند Gateway نمی‌توانند wrapperهای ACPX یکدیگر را جمع‌آوری کنند.
  • رکوردهای قدیمی نشست ACPX در طول مهاجرت همچنان کار می‌کنند.
  • زمان اجرا همچنان متعلق به Plugin می‌ماند؛ هسته جزئیات بسته ACPX را یاد نمی‌گیرد.

غیرهدف‌ها

  • جایگزین کردن ACPX یا تغییر سطح فرمان عمومی /acp.
  • منتقل کردن رفتار adapter ویژه فروشنده ACP به هسته.
  • الزام کاربران به پاک‌سازی دستی وضعیت پیش از ارتقا.
  • کاری که cancel نشست‌های ACP قابل استفاده دوباره را ببندد.

مدل هدف

هویت نمونه Gateway

هر فرایند Gateway باید یک شناسه نمونه زمان اجرای پایدار داشته باشد:

type GatewayInstanceId = string;

این شناسه می‌تواند هنگام راه‌اندازی Gateway تولید شود و برای عمر همان نصب در وضعیت ماندگار شود. این یک راز امنیتی نیست؛ یک متمایزکننده مالکیت است که برای جلوگیری از اشتباه گرفتن فرایندهای ACP یک Gateway با فرایندهای Gateway دیگر استفاده می‌شود.

مالکیت نشست ACP

هر نشست ACP ایجادشده باید فراداده مالکیت نرمال‌شده داشته باشد:

type AcpSessionOwner = {
  sessionKey: string;
  spawnedBy?: string;
  parentSessionKey?: string;
  ownerSessionKey: string;
  agentId: string;
  backend: "acpx";
  gatewayInstanceId: GatewayInstanceId;
  createdAt: number;
};

Gateway باید این فیلدها را روی ردیف‌های نشست، هرجا شناخته شده‌اند، برگرداند. فیلتر کردن نمایانی باید یک بررسی خالص روی فراداده ردیف باشد:

canSeeSessionRow({
  row,
  requesterSessionKey,
  visibility,
  a2aPolicy,
});

این کار فراخوانی‌های ثانویه پنهان sessions.list({ spawnedBy }) را از بررسی‌های نمایانی حذف می‌کند. یک فرزند ACP بین‌عاملی ایجادشده متعلق به درخواست‌کننده است چون ردیف چنین می‌گوید، نه چون یک پرس‌وجوی دوم اتفاقا آن را پیدا می‌کند.

اجاره‌های فرایند ACPX

هر اجرای wrapper تولیدشده باید یک رکورد اجاره ایجاد کند:

type AcpxProcessLease = {
  leaseId: string;
  gatewayInstanceId: GatewayInstanceId;
  sessionKey: string;
  wrapperRoot: string;
  wrapperPath: string;
  rootPid: number;
  processGroupId?: number;
  commandHash: string;
  startedAt: number;
  state: "open" | "closing" | "closed" | "lost";
};

فرایند wrapper باید شناسه اجاره و شناسه نمونه Gateway را در محیط خود دریافت کند:

OPENCLAW_ACPX_LEASE_ID=...
OPENCLAW_GATEWAY_INSTANCE_ID=...

وقتی پلتفرم اجازه می‌دهد، راستی‌آزمایی باید فراداده زنده فرایند را ترجیح دهد که با نقل‌قول‌گذاری فرمان‌ها اشتباه گرفته نمی‌شود:

  • PID ریشه هنوز وجود دارد
  • مسیر زنده wrapper زیر wrapperRoot است
  • گروه فرایند، وقتی در دسترس است، با اجاره مطابقت دارد
  • محیط، وقتی خواندنی است، شامل شناسه اجاره مورد انتظار است
  • هش فرمان یا مسیر فایل اجرایی با اجاره مطابقت دارد

اگر فرایند زنده قابل راستی‌آزمایی نباشد، پاک‌سازی به‌صورت بسته شکست می‌خورد.

کنترل‌کننده چرخه عمر

یک کنترل‌کننده چرخه عمر ACPX معرفی کنید که مالک اجاره‌های فرایند و سیاست پاک‌سازی باشد:

interface AcpxLifecycleController {
  ensureSession(input: AcpRuntimeEnsureInput): Promise<AcpRuntimeHandle>;
  cancelTurn(handle: AcpRuntimeHandle): Promise<void>;
  closeSession(input: {
    handle: AcpRuntimeHandle;
    discardPersistentState?: boolean;
    reason?: string;
  }): Promise<void>;
  reapStartupOrphans(): Promise<void>;
  verifyOwnedTree(lease: AcpxProcessLease): Promise&lt;OwnedProcessTree | null&gt;;
}

cancelTurn فقط درخواست لغو نوبت را می‌دهد. نباید فرایندهای wrapper یا adapter قابل استفاده دوباره را جمع‌آوری کند.

closeSession اجازه جمع‌آوری دارد، اما فقط پس از بارگذاری رکورد نشست، بارگذاری اجاره، و راستی‌آزمایی اینکه درخت فرایند زنده هنوز متعلق به همان اجاره است.

reapStartupOrphans از اجاره‌های باز در وضعیت شروع می‌کند. ممکن است از جدول فرایند برای یافتن فرزندان استفاده کند، اما نباید ابتدا فرمان‌های دلخواه شبیه ACP را اسکن کند و سپس تصمیم بگیرد که احتمالا مال ما هستند.

قرارداد Wrapper

wrapperهای تولیدشده باید کوچک بمانند. آن‌ها باید:

  • adapter را در یک گروه فرایند، هرجا پشتیبانی می‌شود، شروع کنند
  • سیگنال‌های خاتمه عادی را به گروه فرایند ارسال کنند
  • مرگ والد را تشخیص دهند
  • هنگام مرگ والد، SIGTERM ارسال کنند، سپس wrapper را زنده نگه دارند تا جایگزین SIGKILL اجرا شود
  • وقتی در دسترس است، PID ریشه و شناسه گروه فرایند را به کنترل‌کننده چرخه عمر گزارش کنند

wrapperها نباید درباره سیاست نشست تصمیم بگیرند. آن‌ها فقط پاک‌سازی درخت فرایند محلی را برای گروه adapter خودشان اعمال می‌کنند.

قرارداد نمایانی نشست

نمایانی باید از مالکیت نرمال‌شده ردیف استفاده کند:

type SessionVisibilityInput = {
  requesterSessionKey: string;
  row: {
    key: string;
    agentId: string;
    ownerSessionKey?: string;
    spawnedBy?: string;
    parentSessionKey?: string;
  };
  visibility: "self" | "tree" | "agent" | "all";
  a2aPolicy: AgentToAgentPolicy;
};

قواعد:

  • self: فقط نشست درخواست‌کننده.
  • tree: نشست درخواست‌کننده به‌علاوه ردیف‌هایی که متعلق به درخواست‌کننده‌اند یا از او ایجاد شده‌اند.
  • all: همه ردیف‌های همان عامل، ردیف‌های بین‌عاملی مجاز توسط a2a، و ردیف‌های بین‌عاملی ایجادشده متعلق به درخواست‌کننده حتی وقتی a2a عمومی غیرفعال است.
  • agent: فقط همان عامل، مگر اینکه یک رابطه مالکیت صریح بگوید ردیف به درخواست‌کننده تعلق دارد.

این کار tree و all را یکنواخت می‌کند: all نباید فرزند متعلق به درخواست‌کننده‌ای را پنهان کند که tree نشان می‌دهد.

برنامه مهاجرت

مرحله ۱: افزودن هویت و اجاره‌ها

  • gatewayInstanceId را به وضعیت Gateway اضافه کنید.
  • یک فروشگاه اجاره ACPX زیر دایرکتوری وضعیت ACPX اضافه کنید.
  • پیش از ایجاد یک wrapper تولیدشده، یک اجاره بنویسید.
  • leaseId را روی رکوردهای جدید نشست ACPX ذخیره کنید.
  • فیلدهای موجود PID و فرمان را برای رکوردهای قدیمی نگه دارید.

مرحله ۲: پاک‌سازی اجاره‌محور

  • پاک‌سازی بستن را تغییر دهید تا ابتدا leaseId را بارگذاری کند.
  • پیش از سیگنال دادن، مالکیت فرایند زنده را در برابر اجاره راستی‌آزمایی کنید.
  • جایگزین فعلی PID ریشه و ریشه wrapper را فقط برای رکوردهای قدیمی نگه دارید.
  • پس از پاک‌سازی راستی‌آزمایی‌شده، اجاره‌ها را closed علامت‌گذاری کنید.
  • وقتی فرایند پیش از پاک‌سازی از بین رفته است، اجاره‌ها را lost علامت‌گذاری کنید.

مرحله ۳: جمع‌آوری هنگام راه‌اندازی به‌صورت اجاره‌محور

  • جمع‌آوری هنگام راه‌اندازی اجاره‌های باز را اسکن می‌کند.
  • برای هر اجاره، فرایند ریشه را راستی‌آزمایی کنید و فرزندان را جمع‌آوری کنید.
  • درخت‌های راستی‌آزمایی‌شده را از فرزندان به والد جمع‌آوری کنید.
  • اجاره‌های قدیمی closed و lost را با یک پنجره نگهداری محدود منقضی کنید.
  • اسکن نشانگر فرمان را فقط به‌عنوان یک جایگزین موقت قدیمی نگه دارید، که در صورت امکان با ریشه wrapper و نمونه Gateway محافظت می‌شود.

مرحله ۴: ردیف‌های مالکیت نشست

  • فراداده مالکیت را به ردیف‌های نشست Gateway اضافه کنید.
  • به نویسنده‌های ACPX، عامل فرعی، کار پس‌زمینه و فروشگاه نشست بیاموزید که ownerSessionKey یا spawnedBy را پر کنند.
  • بررسی‌های نمایانی نشست را به استفاده از فراداده ردیف تبدیل کنید.
  • جست‌وجوهای ثانویه sessions.list({ spawnedBy }) در زمان نمایانی را حذف کنید.

مرحله ۵: حذف اکتشاف‌های قدیمی

پس از یک پنجره انتشار:

  • اتکا به رشته‌های ذخیره‌شده فرمان ریشه برای پاک‌سازی ACPX غیرقدیمی را متوقف کنید
  • اسکن‌های نشانگر فرمان هنگام راه‌اندازی را حذف کنید
  • جست‌وجوهای فهرستی جایگزین نمایانی را حذف کنید
  • رفتار دفاعی شکست بسته را برای اجاره‌های مفقود یا غیرقابل راستی‌آزمایی نگه دارید

آزمون‌ها

دو مجموعه جدول‌محور اضافه کنید.

شبیه‌ساز چرخه عمر فرایند:

  • PID توسط فرایند نامرتبطی دوباره استفاده شده است
  • PID توسط ریشه wrapper یک Gateway دیگر دوباره استفاده شده است
  • فرمان ذخیره‌شده wrapper با نقل‌قول shell است، فرمان زنده ps نیست
  • فرزند adapter خارج می‌شود، نوه در گروه فرایند باقی می‌ماند
  • جایگزین SIGTERM هنگام مرگ والد به SIGKILL می‌رسد
  • فهرست‌گیری فرایند در دسترس نیست
  • اجاره کهنه با فرایند مفقود
  • یتیم راه‌اندازی با wrapper، فرزند adapter و نوه

ماتریس نمایانی نشست:

  • self، tree، agent، all
  • a2a فعال و غیرفعال
  • ردیف همان عامل
  • ردیف بین‌عاملی
  • ردیف ACP بین‌عاملی ایجادشده متعلق به درخواست‌کننده
  • درخواست‌کننده sandbox‌شده که به tree محدود شده است
  • کنش‌های فهرست، تاریخچه، ارسال و وضعیت

ناوردای مهم: فرزند ایجادشده متعلق به درخواست‌کننده هرجا نمایانی پیکربندی‌شده درخت نشست درخواست‌کننده را شامل شود نمایان است، و all از tree کم‌توان‌تر نیست.

نکات سازگاری

رکوردهای قدیمی نشست ممکن است leaseId نداشته باشند. آن‌ها باید از مسیر پاک‌سازی قدیمی شکست بسته استفاده کنند:

  • نیازمند یک فرایند ریشه زنده
  • وقتی انتظار wrapper تولیدشده می‌رود، نیازمند مالکیت ریشه wrapper
  • برای ریشه‌های غیر wrapper نیازمند توافق فرمان
  • هرگز فقط بر اساس فراداده ذخیره‌شده کهنه PID سیگنال ندهید

اگر یک رکورد قدیمی قابل راستی‌آزمایی نباشد، آن را به حال خود بگذارید. پاک‌سازی اجاره هنگام راه‌اندازی و پنجره انتشار بعدی باید در نهایت جایگزین را بازنشسته کنند.

معیارهای موفقیت

  • بستن یک نشست قدیمی یا کهنه ACPX نمی‌تواند فرایند Gateway دیگری را بکشد.
  • مرگ والد نوه‌های adapter سرسخت را در حال اجرا باقی نمی‌گذارد.
  • cancel نوبت فعال را بدون بستن نشست‌های قابل استفاده دوباره قطع می‌کند.
  • sessions_list می‌تواند فرزندان ACP بین‌عاملی متعلق به درخواست‌کننده را زیر هر دو tree و all نشان دهد.
  • پاک‌سازی هنگام راه‌اندازی با اجاره‌ها هدایت می‌شود، نه اسکن‌های گسترده رشته فرمان.
  • آزمون‌های متمرکز ماتریس فرایند و نمایانی، همه موارد مرزی را پوشش می‌دهند که پیش‌تر به اصلاح‌های موردی در بازبینی نیاز داشتند.