Skip to content

Commit fa98e83

Browse files
committed
기능: 자동화 자원 무잔류 계약을 제품화
각 자원 소유자가 bounded count를 직접 보고하게 했다. Control, MCP, FrameSpace, Python adapter가 같은 resources 영수증을 쓴다. detach가 port와 transport의 session handle을 즉시 회수한다. 반복 관찰과 행동 뒤 자원, process, 임시 profile이 기준선으로 돌아온다. 북극성에 브라우저 눈과 팔 축과 다음 수를 등록했다. 삭제된 과거 evidence 경로를 현재 gate로 교체했다. npm test가 원장의 score, next, lane, evidence와 사다리를 강제한다. 검증: npm run test:automation-lifecycle 20/20 GREEN 검증: session 삭제 제거 음성 변형 0/1 RED, sessions=1 검증: npm run test:mcp-product 25/25 GREEN 검증: npm run test:frame-space 24/24 GREEN 검증: npm run test:python-sdk 5/5 GREEN 검증: npm run test:control-product 26/26 GREEN 검증: npm run test:browser-control 74/74 GREEN 검증: npm run test:browser-control-stress 48 actions GREEN 검증: npm run test:apx 11/11 GREEN 검증: npm run test:contracts 36 suites GREEN 검증: npm run test:types GREEN 검증: npm run test:package 7 files GREEN 검증: npm run skills:check GREEN 검증: northStar missing evidence 음성 변형 12/13 RED 검증: npm test 13/13 GREEN
1 parent dfc60f9 commit fa98e83

41 files changed

Lines changed: 643 additions & 59 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ jobs:
121121
- run: npm run test:browser-control-stress
122122
env:
123123
PYPROC_BROWSER: /usr/bin/google-chrome
124+
- run: npm run test:automation-lifecycle
125+
env:
126+
PYPROC_BROWSER: /usr/bin/google-chrome
124127

125128
# 실제 Microsoft Edge의 Windows 배포 경계. 설치 tarball/골든 경로와 핵심 브라우저
126129
# 계약을 Chrome과 독립해 문다. examples는 Chrome의 동일 쇼케이스를 그대로 반복해 비용이
@@ -177,6 +180,7 @@ jobs:
177180
- run: npm run test:experience-verification
178181
- run: npm run test:browser-control
179182
- run: npm run test:browser-control-stress
183+
- run: npm run test:automation-lifecycle
180184
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
181185
if: always()
182186
with:

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ continued with a continuation-only `automation.observe` call. Every page carries
139139
snapshot receipt, complete-inventory digest, and observation evidence binding. A document replacement rejects
140140
the old continuation instead of treating a partial prefix as complete.
141141

142+
`automation.space.inspect` also returns one provider-neutral `resources` snapshot. After deleting owned
143+
artifacts, detaching sessions, and closing owned targets, every counter returns to the starting baseline. A new
144+
isolated profile therefore returns zero for targets, sessions, locators, continuations, watchers, artifacts,
145+
perception ledgers, transport sessions, pending commands, and listeners.
146+
142147
Start with [Machine Entrance](skills/use-pyproc-machine/references/machine-entrance.md), then use the
143148
[JavaScript Control SDK](skills/control-pyproc/references/javascript-control.md), the
144149
[Python SDK](skills/control-pyproc/references/python-sdk.md), or the complete

mainPlan/9-agentComputerStandardReadiness/README.md

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414

1515
| 질문 | 현재 판정 | 실사용 증거 | 완성까지 남은 것 |
1616
|---|---|---|---|
17-
| agent 진입점 | 매우 강함, 장기 수명주기 검증은 계속 | exact install 뒤 package engine 자동 선택, effect-free doctor, 네 adapter의 같은 CPython 첫 결과가 공개 계약으로 완결 | M2의 반복 작업과 정리 수명주기 |
18-
| 눈과 팔 | 강함, 완성 아님 | APX Situation이 링크를 식별하고 proof-carrying click 뒤 `Smallest start` 출현을 confirmed로 봉인. 현재 main은 첫 문서 교체를 effect 재시도 없이 수렴 | 장기 반복 수명주기와 실제 GPU visual oracle 확대 |
17+
| agent 진입점 | 매우 강함, 장기 수명주기 검증은 계속 | exact install 뒤 package engine 자동 선택, effect-free doctor, 네 adapter의 같은 CPython 첫 결과가 공개 계약으로 완결 | M2의 wrong-effect 없는 자동 수렴 상한 |
18+
| 눈과 팔 | 강함, 완성 아님, 북극성 8.8 | APX Situation이 링크를 식별하고 proof-carrying click 뒤 `Smallest start` 출현을 confirmed로 봉인. 설치 제품 20회가 모든 소유 자원을 기준선으로 되돌림 | stale, ambiguous, occluded 자동 수렴과 실제 GPU visual oracle 확대 |
1919
| 비-agent 컴퓨팅 몸체 | 훌륭한 브라우저 컴퓨터, 로컬 OS 완전 대체는 아님 | owned CPython, worker process, OPFS disk, checkpoint, Machine image, Python과 x86 guest gate | 임의 native wheel, shared-memory thread, wasm 도구층, Node guest, quota 축출 계약 |
2020
| 단독 자립성 | Python 기본 Machine은 높음, 전체 WebComputer는 미완성 | source-built CPython과 stdlib가 npm에 포함되고 기본 부팅의 제3자 요청은 0 | x86 emulator와 firmware의 독립 재현, GPU 실기 CI, 브라우저 범위 확대 |
2121
| 웹 표준 후보 가능성 | 기반은 있음, 후보라고 부르기에는 이름 | WebAssembly, Worker, cross-origin isolation, bucket file system 같은 표준 기반 위에 제품 계약이 동작 | vendor-neutral specification, 독립 구현, WPT형 conformance, 공개 incubation과 wide review |
@@ -101,7 +101,9 @@ implementation experience는 독립적이고 상호운용 가능한 구현, 저
101101
continuation, 5분 TTL, 10,000 node와 16 MiB 전체 상한, page와 prefix 및 전체 digest, screenshot과 event
102102
evidence binding을 NativeCdpSpace와 FrameSpace, Control, MCP, JavaScript와 Python SDK에 같은 의미로 둔다.
103103
문서 교체는 `AUTOMATION_OBSERVATION_CONTINUATION_STALE`로 일부 결과를 complete로 승격하지 않는다.
104-
- 반복 Situation, screenshot, proof-carrying action, artifact cleanup에서 handle과 process 잔류 0을 증명한다.
104+
- 완료: 반복 Situation, screenshot, proof-carrying action, artifact cleanup에서 handle과 process 잔류 0을
105+
증명한다. 각 owner가 직접 센 자원을 `automation.space.inspect.resources`로 합성하고, 격리 profile의
106+
packed Control 제품 20회와 NativeCdpSpace, FrameSpace, MCP, Python adapter가 0 수렴을 검증한다.
105107
- stale, ambiguous, occluded, navigation 교체를 wrong effect 없이 자동 수렴시키는 상한을 고정한다.
106108
- hardware GPU runner에서 pixel 결과 oracle을 추가해 수동 증거 상한을 제거한다.
107109

@@ -266,8 +268,42 @@ implementation experience는 독립적이고 상호운용 가능한 구현, 저
266268
- 음성 증거: consumed token, TTL 만료, item 상한, 다른 document epoch를 모두 고유 오류로 거부한다.
267269
navigation 뒤 continuation은 NativeCdpSpace와 FrameSpace 설치 제품 모두 stale, `notSent`, 비재시도로
268270
종결되고 retained inventory state는 0이다.
269-
- 정식 게이트: contract 36 suites, FrameSpace 23개, JavaScript Control 26개, 설치 MCP 24개, Python wheel과
270-
source distribution 5개가 GREEN이다. 전체 `npm test` 12개와 package gate 7개 파일도 GREEN이다.
271-
- 다음 직렬 작업: 반복 Situation, screenshot, proof-carrying action, artifact cleanup을 장시간 돌리고 매
272-
iteration의 locator, continuation, watcher, artifact, target, process, profile 수가 기준선으로 돌아오는지
273-
검증한다.
271+
- 정식 게이트: contract 36 suites, FrameSpace 24개, JavaScript Control 26개, 설치 MCP 25개, Python wheel과
272+
source distribution 5개가 GREEN이다. 전체 `npm test` 13개와 package gate 7개 파일도 GREEN이다.
273+
- 후속 판정: 반복 Situation, screenshot, proof-carrying action과 artifact cleanup의 장기 무잔류는 아래
274+
6절에서 완료했다. 다음 직렬 작업은 wrong-effect 없는 자동 수렴 상한이다.
275+
276+
## 6. 해결 판정: automation 장기 수명주기 무잔류
277+
278+
- 첫 불일치: 기존 `browserInspect.sessions`는 연결된 session만 세어 detach 뒤 0을 반환했지만,
279+
`BrowserControlPort._sessions``NodeCdpTransport._sessions`에는 이미 detach된 session handle이 남았다.
280+
perception identity, timeline, world, Situation history와 capability, CDP pending과 listener도 공개
281+
inspect에서 보이지 않아 기존 stress green은 무잔류 증거가 아니었다.
282+
- 실측 순서: exact packed `pyproc@0.0.22` source product, Windows 11, Edge 151, Node.js 22에서 공개
283+
`pyproc/control`만 사용했다. 매 회 새 target을 열고 Situation과 native visual screenshot을 얻어 artifact를
284+
삭제하고, broker capability로 click한 뒤 DOM postcondition을 confirmed로 봉인했다. action screenshot도
285+
digest 검증 뒤 삭제하고 detach와 target close를 수행했다.
286+
- 첫 실행: effect와 두 artifact cleanup까지 성공했으나 기존 공개 inspect에 완전한 `resources`가 없어
287+
첫 회 0/20에서 RED였다.
288+
- 수정: detach event와 명시적 detach가 port와 transport Map에서 session을 즉시 삭제한다. perception
289+
identity, timeline, world, capability와 sensor, CDP connection, transport, port, broker, artifact,
290+
observation, lifecycle owner가 자기 bounded count만 보고하고 NativeCdpSpace와 FrameSpace가 같은
291+
provider-neutral receipt로 합성한다.
292+
- 무손실 증거: 정식 `test:automation-lifecycle`은 packed 제품 20/20회에서 target, session, locator,
293+
continuation, watcher, artifact, perception ledger, transport session, pending command와 listener를 매 회
294+
0으로 되돌렸다. client close 뒤 Control process가 종료됐고 새 임시 browser profile도 0개였다.
295+
- 게이트 이빨: `BrowserControlPort`의 session 삭제 한 줄을 제거한 음성 변형은 첫 회에
296+
`sessions=1`을 보고 0/1 RED였다. 정리 코드를 복원하면 같은 설치 제품 20/20이 GREEN이다.
297+
- adapter 증거: 설치 MCP와 FrameSpace는 detach와 target close 뒤 같은 resource vector를 0으로 확인한다.
298+
clean wheel과 source distribution의 Python SDK도 `closeTarget()` 뒤 top-level, transport, perception count
299+
전체를 독립 확인한다.
300+
- adapter 첫 RED: 기존 설치 MCP 여정은 읽고 남겨 둔 APX와 PNG, JPEG, WebP artifact 네 개 때문에
301+
24/25에서 `artifacts=4`, `artifactBytes=83069`를 보고 실패했다. 모든 소유 artifact를 명시 삭제한 뒤
302+
전체 resource vector가 0이 되어 25/25 GREEN이 됐다.
303+
- 원장 무결성: `tests/northStar.mjs`에 눈과 팔 축을 8.8로 추가하고 다음 수를
304+
`wrongEffectFreeConvergence`로 고정했다. 삭제된 과거 probe 여덟 경로는 현재 정식 gate로 교체했으며,
305+
전체 axis의 score, next, browser lane, 실행 lane, evidence 실존과 사다리 연속성을 `npm test`가 검사한다.
306+
- 원장 gate 이빨: 새 축의 lifecycle evidence를 존재하지 않는 `.missing` 경로로 바꾼 음성 변형은
307+
`agentBrowserAutomation: missing evidence`를 보고 12/13 RED였고, 복원 뒤 13/13 GREEN이다.
308+
- 다음 직렬 작업: stale, ambiguous, occluded, navigation 교체를 wrong effect 없이 자동 수렴시키는 횟수와
309+
시간 상한을 고정한다.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
"test:experience-verification": "node tests/browser/experienceVerificationProduct.mjs",
112112
"test:browser-control": "node tests/browser/browserControl.mjs",
113113
"test:browser-control-stress": "node tests/browser/browserControlStress.mjs",
114+
"test:automation-lifecycle": "node tests/browser/automationLifecycleProduct.mjs",
114115
"test:browser": "node tests/browser/run.mjs",
115116
"test:examples": "node tests/browser/examples.mjs",
116117
"bench:speed": "node tests/browser/speedBench.mjs",

pythonSdk/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ without a JavaScript application layer or a raw CDP handle.
3232
Legacy `client.observe(...)` results also support single-use, document-epoch-bound `continuationRef` values for
3333
complete semantic inventories larger than one 1,000-node page. Continue with no repeated first-page options and
3434
accept completeness only when the final `inventory.complete` and full digest agree.
35+
After artifact deletion, session detach, and `closeTarget(...)`, use `inspectSpace().output["resources"]` as the
36+
provider-neutral cleanup receipt. A new isolated profile returns zero for every top-level, transport, and
37+
perception resource counter.
3538

3639
Repository experience verification uses `auditExperience`, `verifyExperience`, and `replayEvidencePack` over the
3740
same Control Protocol. The repository

scripts/automationSpace/frameSpace.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,11 @@ export class FrameSpace {
177177
requestId: typeof requestId === "string" && requestId ? requestId : `frame:${++this._sequence}`,
178178
});
179179
if (operation === "automation.space.inspect") {
180+
const perception = this._perception.inspect();
180181
return Object.freeze({ ...output, targetOrigins: Object.freeze([...this.config.targetOrigins]),
181182
viewport: this.config.viewport, compatibility: Object.freeze({ family: "chromium", version: "embedded" }),
182-
perception: this._perception.inspect() });
183+
perception,
184+
resources: Object.freeze({ ...output.resources, perception: perception.resources }) });
183185
}
184186
if (operation === "automation.session.detach") this._perception.dropSession(input.sessionRef);
185187
return output;

scripts/automationSpace/frameSpacePage.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,13 @@ export class FrameSpacePage {
9898
});
9999
}
100100

101-
inspect() {
101+
async inspect() {
102102
this._reap();
103+
const targetResources = await Promise.all([...this.targets.values()]
104+
.map((target) => this._call(target, "observe.inspect", {})));
105+
const locators = targetResources.reduce((total, resource) => total + (resource.locators || 0), 0);
106+
const pending = [...this.targets.values()].reduce((total, target) => total + target.pending.size, 0);
107+
const semanticInventory = this.semanticInventory.inspect();
103108
return Object.freeze({
104109
transport: "messageChannel",
105110
sandbox: "allow-scripts allow-forms",
@@ -109,7 +114,24 @@ export class FrameSpacePage {
109114
targetCount: this.targets.size,
110115
sessionCount: this.sessions.size,
111116
artifactCount: this.artifacts.size,
112-
semanticInventory: this.semanticInventory.inspect(),
117+
semanticInventory,
118+
resources: Object.freeze({
119+
targets: this.targets.size,
120+
ownedTargets: this.targets.size,
121+
sessions: this.sessions.size,
122+
locators,
123+
quarantinedSessions: 0,
124+
semanticInventories: semanticInventory.active,
125+
continuations: semanticInventory.continuations,
126+
observationListeners: 0,
127+
observationEvents: 0,
128+
lifecycleSessions: 0,
129+
lifecycleWatchers: 0,
130+
lifecycleQueuedEvents: 0,
131+
artifacts: this.artifacts.size,
132+
artifactBytes: this.totalArtifactBytes,
133+
transport: Object.freeze({ sessions: this.targets.size, pending, listeners: this.targets.size }),
134+
}),
113135
});
114136
}
115137

scripts/automationSpace/frameSpaceTarget.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@
426426
}
427427
if (operation === "observe") return snapshot(input);
428428
if (operation === "observe.epoch") return Object.freeze({ targetEpoch, url: location.href, title: document.title });
429+
if (operation === "observe.inspect") return Object.freeze({ locators: locatorByRef.size });
429430
if (operation === "perception.capture") return perceptionSnapshot(input);
430431
if (operation === "action.snapshot") return snapshot(input);
431432
if (operation === "action.screenshot") return screenshot();

scripts/browserControl/browserControlBroker.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,8 @@ export class NodeBrowserControlBroker {
206206

207207
inspect() {
208208
return Object.freeze({ transport: "node-cdp", listener: null, compatibility: this.compatibility,
209-
viewport: this._viewport, ...this.port.inspect() });
209+
viewport: this._viewport, ownedTargets: this._ownedTargets.size,
210+
connection: this._connection.inspect?.() || null, ...this.port.inspect() });
210211
}
211212

212213
close() { return this.port.close(); }

scripts/browserControl/browserControlPort.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,9 @@ export class BrowserControlPort {
416416
closed: this._closed,
417417
targets: this._targets.size,
418418
sessions: [...this._sessions.values()].filter((session) => session.state === "attached").length,
419+
retainedSessions: this._sessions.size,
419420
popupCaptures: this._popupCaptures.size,
421+
transport: this._transport.inspect?.() || null,
420422
policy: this.policy.inspect(),
421423
});
422424
}
@@ -478,7 +480,8 @@ export class BrowserControlPort {
478480
method = "Transport.contextReplaced";
479481
params = { sourceMethod: event.method, contextEpoch: session.contextEpoch };
480482
}
481-
if (method === "Transport.detached") this._markDetached(session, params.reason || "transport_detach");
483+
const detachedListeners = method === "Transport.detached" ? [...session.listeners] : null;
484+
if (detachedListeners) this._markDetached(session, params.reason || "transport_detach");
482485
if (session.authorizationState !== "verified"
483486
&& method !== "Transport.contextReplaced" && method !== "Transport.detached") return;
484487
if (!this.policy.allowsEvent(method)) return;
@@ -488,14 +491,17 @@ export class BrowserControlPort {
488491
params: Object.freeze({ ...params }),
489492
sessionRef: this._sessionRef(session),
490493
});
491-
for (const listener of [...session.listeners]) listener(normalized);
494+
for (const listener of detachedListeners || [...session.listeners]) listener(normalized);
492495
}
493496

494497
_markDetached(session, reason, removeTransportListener = true) {
495498
if (session.state === "detached") return;
496499
session.state = "detached";
497500
session.detachReason = reason;
498501
if (removeTransportListener) session.unsubscribe?.();
502+
session.unsubscribe = null;
503+
session.listeners.clear();
504+
this._sessions.delete(session.sessionId);
499505
}
500506

501507
_error(code, message, options = {}) {

0 commit comments

Comments
 (0)