---
read_when:
    - การปรับโครงสร้างวงจรชีวิตของเซสชัน ACP หรือการล้างกระบวนการ ACPX
    - การดีบักกระบวนการกำพร้า ACPX, การนำ PID กลับมาใช้ซ้ำ หรือความปลอดภัยในการล้างข้อมูลเมื่อมีหลาย Gateway
    - การเปลี่ยนการมองเห็นของ sessions_list สำหรับเซสชัน ACP หรือเซสชันตัวแทนย่อยที่ถูกสร้างขึ้น
    - การออกแบบเมทาดาทาความเป็นเจ้าของสำหรับงานเบื้องหลัง เซสชัน ACP หรือการลีสกระบวนการ
sidebarTitle: ACP lifecycle refactor
summary: แผนการย้ายระบบเพื่อทำให้ความเป็นเจ้าของเซสชัน ACP และกระบวนการ ACPX ชัดเจน
title: การรีแฟกเตอร์วงจรชีวิต ACP
x-i18n:
    generated_at: "2026-05-07T13:26:09Z"
    model: gpt-5.5
    provider: openai
    source_hash: b7f4ee447e0b436601c68251c26c1b897a642f6a8b1886d18647b62817996792
    source_path: refactor/acp.md
    workflow: 16
    postprocess_version: locale-links-v1
---

วงจรชีวิตของ ACP ทำงานได้ในปัจจุบัน แต่ส่วนใหญ่ถูกอนุมานย้อนหลังมากเกินไป
การล้างโปรเซสสร้างความเป็นเจ้าของขึ้นใหม่จาก PID, สตริงคำสั่ง, เส้นทาง wrapper
และตารางโปรเซสที่กำลังทำงานอยู่ การมองเห็นเซสชันสร้างความเป็นเจ้าของขึ้นใหม่
จากสตริง session-key บวกกับการค้นหา `sessions.list({ spawnedBy })` ขั้นรอง
วิธีนี้ทำให้การแก้ไขแบบแคบทำได้ แต่ก็ทำให้พลาดกรณีขอบได้ง่ายเช่นกัน:
การนำ PID กลับมาใช้ซ้ำ, คำสั่งที่ถูก quote, โปรเซสรุ่นหลานของอะแดปเตอร์, รากสถานะของหลาย Gateway,
`cancel` เทียบกับ `close`, และการมองเห็นแบบ `tree` เทียบกับ `all` ล้วนกลายเป็นคนละจุด
ที่ต้องค้นพบกฎความเป็นเจ้าของชุดเดียวกันใหม่

การปรับโครงสร้างนี้ทำให้ความเป็นเจ้าของเป็นสิ่งชั้นหนึ่ง เป้าหมายไม่ใช่พื้นผิวผลิตภัณฑ์ ACP ใหม่
แต่เป็นสัญญาภายในที่ปลอดภัยขึ้นสำหรับพฤติกรรม ACP และ ACPX ที่มีอยู่

## เป้าหมาย

- การล้างจะไม่ส่งสัญญาณไปยังโปรเซส เว้นแต่หลักฐานสดปัจจุบันตรงกับ
  สิทธิ์การถือครองที่ OpenClaw เป็นเจ้าของ
- `cancel`, `close`, และการเก็บกวาดตอนเริ่มต้นมีเจตนาวงจรชีวิตที่แยกจากกัน
- `sessions_list`, `sessions_history`, `sessions_send`, และการตรวจสอบสถานะใช้
  โมเดลเซสชันที่ผู้ร้องขอเป็นเจ้าของเดียวกัน
- การติดตั้งหลาย Gateway ไม่สามารถเก็บกวาด wrapper ของ ACPX ของกันและกันได้
- ระเบียนเซสชัน ACPX เก่ายังคงทำงานระหว่างการย้ายข้อมูล
- รันไทม์ยังคงเป็นของ Plugin; core ไม่เรียนรู้รายละเอียดแพ็กเกจ ACPX

## สิ่งที่ไม่ใช่เป้าหมาย

- แทนที่ ACPX หรือเปลี่ยนพื้นผิวคำสั่ง `/acp` สาธารณะ
- ย้ายพฤติกรรมอะแดปเตอร์ ACP เฉพาะผู้ขายเข้าไปใน core
- บังคับให้ผู้ใช้ล้างสถานะด้วยตนเองก่อนอัปเกรด
- ทำให้ `cancel` ปิดเซสชัน ACP ที่นำกลับมาใช้ซ้ำได้

## โมเดลเป้าหมาย

### อัตลักษณ์อินสแตนซ์ของ Gateway

แต่ละโปรเซส Gateway ควรมีรหัสอินสแตนซ์รันไทม์ที่เสถียร:

```ts
type GatewayInstanceId = string;
```

สามารถสร้างได้เมื่อ Gateway เริ่มต้น และบันทึกไว้ในสถานะตลอดอายุของ
การติดตั้งนั้น นี่ไม่ใช่ความลับด้านความปลอดภัย แต่เป็นตัวแยกแยะความเป็นเจ้าของที่ใช้
เพื่อหลีกเลี่ยงการสับสนโปรเซส ACP ของ Gateway หนึ่งกับโปรเซสของ Gateway อีกตัว

### ความเป็นเจ้าของเซสชัน ACP

ทุกเซสชัน ACP ที่ถูกสร้างควรมีเมทาดาทาความเป็นเจ้าของที่ทำให้เป็นรูปแบบมาตรฐาน:

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

Gateway ควรคืนค่าฟิลด์เหล่านี้บนแถวเซสชันเมื่อทราบค่าแล้ว
การกรองการมองเห็นควรเป็นการตรวจสอบแบบบริสุทธิ์บนเมทาดาทาของแถว:

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

วิธีนี้นำการเรียก `sessions.list({ spawnedBy })` ขั้นรองที่ซ่อนอยู่ ออกจาก
การตรวจสอบการมองเห็น ลูก ACP ข้ามเอเจนต์ที่ถูกสร้างขึ้นเป็นของผู้ร้องขอ เพราะ
แถวระบุไว้เช่นนั้น ไม่ใช่เพราะมี query ที่สองบังเอิญพบมัน

### สิทธิ์การถือครองโปรเซส ACPX

ทุกการเปิด wrapper ที่สร้างขึ้นควรสร้างระเบียนสิทธิ์การถือครอง:

```ts
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 ใน
สภาพแวดล้อม:

```sh
OPENCLAW_ACPX_LEASE_ID=...
OPENCLAW_GATEWAY_INSTANCE_ID=...
```

เมื่อแพลตฟอร์มรองรับ การยืนยันควรเลือกใช้เมทาดาทาโปรเซสสด
ที่ไม่สับสนได้จากการ quote คำสั่ง:

- PID รากยังคงมีอยู่
- เส้นทาง wrapper สดอยู่ใต้ `wrapperRoot`
- กลุ่มโปรเซสตรงกับสิทธิ์การถือครองเมื่อมีข้อมูล
- สภาพแวดล้อมมีรหัสสิทธิ์การถือครองที่คาดไว้เมื่ออ่านได้
- แฮชคำสั่งหรือเส้นทาง executable ตรงกับสิทธิ์การถือครอง

หากไม่สามารถยืนยันโปรเซสสดได้ การล้างจะปิดทางโดยไม่ดำเนินการ

## ตัวควบคุมวงจรชีวิต

เพิ่มตัวควบคุมวงจรชีวิต ACPX หนึ่งตัวที่เป็นเจ้าของสิทธิ์การถือครองโปรเซสและนโยบาย
การล้าง:

```ts
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<OwnedProcessTree | null>;
}
```

`cancelTurn` ร้องขอการยกเลิก turn เท่านั้น ต้องไม่เก็บกวาด wrapper
หรือโปรเซสอะแดปเตอร์ที่นำกลับมาใช้ซ้ำได้

`closeSession` สามารถเก็บกวาดได้ แต่ต้องทำหลังจากโหลดระเบียนเซสชัน,
โหลดสิทธิ์การถือครอง, และยืนยันว่า tree ของโปรเซสสดยังเป็นของ
สิทธิ์การถือครองนั้นเท่านั้น

`reapStartupOrphans` เริ่มจากสิทธิ์การถือครองที่เปิดอยู่ในสถานะ สามารถใช้ตารางโปรเซส
เพื่อค้นหาลูกหลานได้ แต่ไม่ควรสแกนคำสั่งใดๆ ที่ดูเหมือน ACP โดยพลการก่อน
แล้วค่อยตัดสินว่าน่าจะเป็นของเรา

## สัญญา Wrapper

wrapper ที่สร้างขึ้นควรมีขนาดเล็กต่อไป ควร:

- เริ่มอะแดปเตอร์ในกลุ่มโปรเซสเมื่อรองรับ
- ส่งต่อสัญญาณ termination ปกติไปยังกลุ่มโปรเซส
- ตรวจจับการตายของ parent
- เมื่อ parent ตาย ให้ส่ง SIGTERM แล้วให้ wrapper ยังทำงานอยู่จนกว่า fallback
  SIGKILL จะทำงาน
- รายงาน PID รากและรหัสกลุ่มโปรเซสกลับไปยังตัวควบคุมวงจรชีวิตเมื่อ
  มีข้อมูลนั้น

wrapper ไม่ควรตัดสินนโยบายเซสชัน มันบังคับใช้เฉพาะการล้าง tree โปรเซสภายใน
สำหรับกลุ่มอะแดปเตอร์ของตัวเองเท่านั้น

## สัญญาการมองเห็นเซสชัน

การมองเห็นควรใช้ความเป็นเจ้าของของแถวที่ทำให้เป็นรูปแบบมาตรฐาน:

```ts
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` เป็นแบบ monotonic: `all` ต้องไม่ซ่อนลูกที่เป็นเจ้าของ
ซึ่ง `tree` จะแสดง

## แผนการย้ายข้อมูล

### เฟส 1: เพิ่มอัตลักษณ์และสิทธิ์การถือครอง

- เพิ่ม `gatewayInstanceId` ไปยังสถานะ Gateway
- เพิ่ม store สิทธิ์การถือครอง ACPX ใต้ไดเรกทอรีสถานะ ACPX
- เขียนสิทธิ์การถือครองก่อนสร้าง wrapper ที่สร้างขึ้น
- เก็บ `leaseId` บนระเบียนเซสชัน ACPX ใหม่
- เก็บฟิลด์ PID และคำสั่งเดิมไว้สำหรับระเบียนเก่า

### เฟส 2: การล้างโดยยึดสิทธิ์การถือครองเป็นหลัก

- เปลี่ยนการล้างตอนปิดให้โหลด `leaseId` ก่อน
- ยืนยันความเป็นเจ้าของโปรเซสสดเทียบกับสิทธิ์การถือครองก่อนส่งสัญญาณ
- เก็บ fallback ของ PID รากและ wrapper-root ปัจจุบันไว้เฉพาะระเบียน legacy
- ทำเครื่องหมายสิทธิ์การถือครองเป็น `closed` หลังการล้างที่ยืนยันแล้ว
- ทำเครื่องหมายสิทธิ์การถือครองเป็น `lost` เมื่อโปรเซสหายไปก่อนการล้าง

### เฟส 3: การเก็บกวาดตอนเริ่มต้นโดยยึดสิทธิ์การถือครองเป็นหลัก

- การเก็บกวาดตอนเริ่มต้นสแกนสิทธิ์การถือครองที่เปิดอยู่
- สำหรับแต่ละสิทธิ์การถือครอง ให้ยืนยันโปรเซสรากและรวบรวมลูกหลาน
- เก็บกวาด tree ที่ยืนยันแล้วโดยจัดการลูกก่อน
- หมดอายุสิทธิ์การถือครอง `closed` และ `lost` เก่าด้วยกรอบการเก็บรักษาที่มีขอบเขต
- เก็บการสแกน command-marker ไว้เป็น fallback legacy ชั่วคราวเท่านั้น โดยมี
  wrapper root และอินสแตนซ์ Gateway คุมเมื่อเป็นไปได้

### เฟส 4: แถวความเป็นเจ้าของเซสชัน

- เพิ่มเมทาดาทาความเป็นเจ้าของไปยังแถวเซสชัน Gateway
- สอนตัวเขียน ACPX, subagent, งานเบื้องหลัง, และ session-store ให้เติม
  `ownerSessionKey` หรือ `spawnedBy`
- แปลงการตรวจสอบการมองเห็นเซสชันให้ใช้เมทาดาทาของแถว
- ลบการค้นหา `sessions.list({ spawnedBy })` ขั้นรองในเวลาตรวจการมองเห็น

### เฟส 5: ลบ heuristic legacy

หลังหนึ่งช่วง release:

- หยุดพึ่งพาสตริงคำสั่งรากที่จัดเก็บไว้สำหรับการล้าง ACPX ที่ไม่ใช่ legacy
- ลบการสแกน command-marker ตอนเริ่มต้น
- ลบการค้นหารายการ fallback สำหรับการมองเห็น
- เก็บพฤติกรรมปิดทางโดยไม่ดำเนินการเชิงป้องกันสำหรับสิทธิ์การถือครองที่หายไปหรือยืนยันไม่ได้

## การทดสอบ

เพิ่มชุดทดสอบแบบ table-driven สองชุด

ตัวจำลองวงจรชีวิตโปรเซส:

- PID ถูกนำกลับมาใช้ซ้ำโดยโปรเซสที่ไม่เกี่ยวข้อง
- PID ถูกนำกลับมาใช้ซ้ำโดย wrapper root ของ Gateway อีกตัว
- คำสั่ง wrapper ที่จัดเก็บไว้ถูก shell-quoted แต่คำสั่ง `ps` สดไม่ใช่
- ลูกของอะแดปเตอร์ออกไปแล้ว แต่หลานยังอยู่ในกลุ่มโปรเซส
- fallback SIGTERM เมื่อ parent ตายไปถึง SIGKILL
- รายการโปรเซสไม่พร้อมใช้งาน
- สิทธิ์การถือครองค้างที่ไม่มีโปรเซส
- orphan ตอนเริ่มต้นที่มี wrapper, ลูกอะแดปเตอร์, และหลาน

เมทริกซ์การมองเห็นเซสชัน:

- `self`, `tree`, `agent`, `all`
- a2a เปิดใช้งานและปิดใช้งาน
- แถวของเอเจนต์เดียวกัน
- แถวข้ามเอเจนต์
- แถว ACP ข้ามเอเจนต์ที่ถูกสร้างและผู้ร้องขอเป็นเจ้าของ
- ผู้ร้องขอแบบ sandboxed ถูกจำกัดไว้ที่ `tree`
- การกระทำ list, history, send, และ status

อินวาเรียนต์สำคัญ: ลูกที่ถูกสร้างและผู้ร้องขอเป็นเจ้าของจะมองเห็นได้ทุกที่
ที่การมองเห็นที่กำหนดค่ารวม tree เซสชันของผู้ร้องขอ และ `all` ไม่ด้อยความสามารถกว่า
`tree`

## หมายเหตุความเข้ากันได้

ระเบียนเซสชันเก่าอาจไม่มี `leaseId` ควรใช้เส้นทางการล้าง legacy
ที่ปิดทางโดยไม่ดำเนินการ:

- ต้องมีโปรเซสรากสด
- ต้องมีความเป็นเจ้าของ wrapper-root เมื่อคาดว่าจะมี wrapper ที่สร้างขึ้น
- ต้องมีความสอดคล้องของคำสั่งสำหรับรากที่ไม่ใช่ wrapper
- ห้ามส่งสัญญาณโดยอิงเฉพาะเมทาดาทา PID ที่จัดเก็บไว้และค้างแล้ว

หากระเบียน legacy ยืนยันไม่ได้ ให้ปล่อยไว้ตามเดิม การล้างสิทธิ์การถือครองตอนเริ่มต้นและ
ช่วง release ถัดไปควรเลิกใช้ fallback ได้ในที่สุด

## เกณฑ์ความสำเร็จ

- การปิดเซสชัน ACPX เก่าหรือค้างไม่สามารถฆ่าโปรเซสของ Gateway อีกตัวได้
- การตายของ parent ไม่ปล่อยให้โปรเซสรุ่นหลานของอะแดปเตอร์ที่ดื้อยังทำงานอยู่
- `cancel` ยกเลิก turn ที่กำลังทำงานโดยไม่ปิดเซสชันที่นำกลับมาใช้ซ้ำได้
- `sessions_list` สามารถแสดงลูก ACP ข้ามเอเจนต์ที่ผู้ร้องขอเป็นเจ้าของภายใต้ทั้ง
  `tree` และ `all`
- การล้างตอนเริ่มต้นขับเคลื่อนด้วยสิทธิ์การถือครอง ไม่ใช่การสแกนสตริงคำสั่งแบบกว้าง
- ชุดทดสอบเมทริกซ์โปรเซสและการมองเห็นแบบเจาะจงครอบคลุมทุกกรณีขอบที่
  ก่อนหน้านี้ต้องใช้การแก้จาก review แบบเฉพาะจุด
