helloagents 3.0.21 → 3.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +3 -2
- package/README_CN.md +3 -2
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/scripts/notify-closeout.mjs +213 -0
- package/scripts/notify.mjs +73 -78
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helloagents",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.22",
|
|
4
4
|
"description": "HelloAGENTS — The orchestration kernel that makes any AI CLI smarter. Adds intelligent routing, quality verification (Ralph Loop), safety guards, and notifications.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HelloWind",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helloagents",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.22",
|
|
4
4
|
"description": "HelloAGENTS — Quality-driven orchestration kernel for AI CLIs with intelligent routing, quality verification (Ralph Loop), safety guards, and notifications.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HelloWind",
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
**A workflow layer for AI coding CLIs: skills, project knowledge, delivery checks, safer config writes, and resumable execution.**
|
|
10
10
|
|
|
11
|
-
[](./package.json)
|
|
12
12
|
[](https://www.npmjs.com/package/helloagents)
|
|
13
13
|
[](./package.json)
|
|
14
14
|
[](./skills)
|
|
@@ -645,6 +645,7 @@ Codex is rules-file driven by default.
|
|
|
645
645
|
- standby creates `~/.codex/helloagents -> ~/.helloagents/helloagents`
|
|
646
646
|
- global mode installs the native local-plugin chain and also loads silent hooks from `~/.codex/hooks.json`
|
|
647
647
|
- Codex hooks only synchronize runtime state and enforce Stop gates; they do not inject HelloAGENTS rules or route text through hook output
|
|
648
|
+
- Codex closeout de-duplicates Stop hooks and native `codex-notify`, so one turn does not notify twice
|
|
648
649
|
- `/goal` remains Codex-native. Enable it explicitly with `helloagents codex goals enable` when long-running plan execution is needed
|
|
649
650
|
- Goal-aware commands resume from `tasks.md`, `contract.json`, and `state_path`; they do not create goals automatically or mark them complete before HelloAGENTS verification and closeout
|
|
650
651
|
|
|
@@ -656,7 +657,7 @@ Run all tests:
|
|
|
656
657
|
npm test
|
|
657
658
|
```
|
|
658
659
|
|
|
659
|
-
The current
|
|
660
|
+
The current suite includes 103 tests and covers:
|
|
660
661
|
|
|
661
662
|
- install, update, uninstall, cleanup, and mode switching
|
|
662
663
|
- Claude, Gemini, and Codex config merge and restore behavior
|
package/README_CN.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
**面向 AI 编码 CLI 的工作流层:技能、知识库、交付检查、更安全的配置写入,以及可恢复的执行流程。**
|
|
10
10
|
|
|
11
|
-
[](./package.json)
|
|
12
12
|
[](https://www.npmjs.com/package/helloagents)
|
|
13
13
|
[](./package.json)
|
|
14
14
|
[](./skills)
|
|
@@ -647,6 +647,7 @@ Codex 默认走规则文件驱动。
|
|
|
647
647
|
- 标准模式创建 `~/.codex/helloagents -> ~/.helloagents/helloagents`
|
|
648
648
|
- 全局模式安装原生本地插件流程,并同样用 `~/.codex/hooks.json` 加载静默 hooks
|
|
649
649
|
- Codex hooks 只做静默运行态同步和 Stop 门禁,不通过 hook 注入 HelloAGENTS 规则或路由说明
|
|
650
|
+
- Codex 收尾会对 Stop hook 和原生 `codex-notify` 去重,避免同一轮重复通知
|
|
650
651
|
- `/goal` 保持 Codex 原生能力;需要长程执行时,用 `helloagents codex goals enable` 显式启用
|
|
651
652
|
- 感知 goal 的命令从 `tasks.md`、`contract.json` 和 `state_path` 恢复;不会自动创建 goal,也不会在 HelloAGENTS 验证和收尾前标记完成
|
|
652
653
|
|
|
@@ -658,7 +659,7 @@ Codex 默认走规则文件驱动。
|
|
|
658
659
|
npm test
|
|
659
660
|
```
|
|
660
661
|
|
|
661
|
-
|
|
662
|
+
当前测试共 103 项,覆盖:
|
|
662
663
|
|
|
663
664
|
- 安装、更新、卸载、清理和模式切换
|
|
664
665
|
- Claude、Gemini、Codex 的配置合并与恢复
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helloagents",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "HelloAGENTS — The orchestration kernel that makes any AI CLI smarter. Adds intelligent routing, quality verification (Ralph Loop), safety guards, and notifications.",
|
|
6
6
|
"author": "HelloWind",
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { dirname } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { getRuntimeEvidencePath, readRuntimeEvidence, writeRuntimeEvidence } from './runtime-artifacts.mjs'
|
|
6
|
+
|
|
7
|
+
export const CODEX_CLOSEOUT_EVIDENCE_FILE = 'codex-native-stop.json'
|
|
8
|
+
const CODEX_CLOSEOUT_LOCK_FILE = 'codex-native-stop.lock'
|
|
9
|
+
const WEAK_KEY_TTL_MS = 10_000
|
|
10
|
+
const LOCK_STALE_MS = 120_000
|
|
11
|
+
|
|
12
|
+
function getTurnId(payload = {}) {
|
|
13
|
+
return String(payload.turnId || payload.turn_id || payload['turn-id'] || '').trim()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getSessionId(payload = {}) {
|
|
17
|
+
return String(payload.sessionId || payload.session_id || payload['session-id'] || '').trim()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeMessage(message = '') {
|
|
21
|
+
return String(message || '')
|
|
22
|
+
.replace(/\s+/g, ' ')
|
|
23
|
+
.trim()
|
|
24
|
+
.slice(0, 240)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hashValue(value = '') {
|
|
28
|
+
return createHash('sha1').update(String(value)).digest('hex').slice(0, 16)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function uniqueKeys(values = []) {
|
|
32
|
+
return Array.from(new Set(values.filter(Boolean)))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readLockPayload(lockPath) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(readFileSync(lockPath, 'utf-8'))
|
|
38
|
+
} catch {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isLockStale(lockPath, now = Date.now()) {
|
|
44
|
+
try {
|
|
45
|
+
const stat = statSync(lockPath)
|
|
46
|
+
return now - stat.mtimeMs > LOCK_STALE_MS
|
|
47
|
+
} catch {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeLockFile(lockPath, payload) {
|
|
53
|
+
mkdirSync(dirname(lockPath), { recursive: true })
|
|
54
|
+
const fd = openSync(lockPath, 'wx')
|
|
55
|
+
try {
|
|
56
|
+
writeFileSync(fd, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
|
|
57
|
+
} finally {
|
|
58
|
+
closeSync(fd)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function releaseLockFile(lockPath) {
|
|
63
|
+
try {
|
|
64
|
+
unlinkSync(lockPath)
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function intersects(left = [], right = []) {
|
|
69
|
+
if (!left.length || !right.length) return false
|
|
70
|
+
const rightSet = new Set(right)
|
|
71
|
+
return left.some((item) => rightSet.has(item))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function buildCodexCloseoutSnapshot({ payload = {}, turnState = null } = {}) {
|
|
75
|
+
const turnId = getTurnId(payload)
|
|
76
|
+
const sessionId = getSessionId(payload)
|
|
77
|
+
const message = normalizeMessage(
|
|
78
|
+
payload.lastAssistantMessage
|
|
79
|
+
|| payload.last_assistant_message
|
|
80
|
+
|| payload['last-assistant-message']
|
|
81
|
+
|| '',
|
|
82
|
+
)
|
|
83
|
+
const messageHash = message ? hashValue(message) : ''
|
|
84
|
+
|
|
85
|
+
const strongKeys = uniqueKeys([
|
|
86
|
+
turnId ? `turn:${turnId}` : '',
|
|
87
|
+
turnState?.key && turnState?.updatedAt
|
|
88
|
+
? `state:${turnState.key}:${turnState.updatedAt}`
|
|
89
|
+
: '',
|
|
90
|
+
])
|
|
91
|
+
const weakKeys = uniqueKeys([
|
|
92
|
+
sessionId && messageHash ? `session-message:${sessionId}:${messageHash}` : '',
|
|
93
|
+
!sessionId && messageHash ? `message:${messageHash}` : '',
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
turnId,
|
|
98
|
+
sessionId,
|
|
99
|
+
messageHash,
|
|
100
|
+
strongKeys,
|
|
101
|
+
weakKeys,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function matchesCodexCloseoutEvidence(evidence, snapshot, now = Date.now()) {
|
|
106
|
+
if (!evidence || typeof evidence !== 'object') return false
|
|
107
|
+
|
|
108
|
+
const strongKeys = Array.isArray(evidence.strongKeys) ? evidence.strongKeys : []
|
|
109
|
+
const weakKeys = Array.isArray(evidence.weakKeys) ? evidence.weakKeys : []
|
|
110
|
+
if (intersects(snapshot.strongKeys, strongKeys)) return true
|
|
111
|
+
|
|
112
|
+
const currentHasStrong = snapshot.strongKeys.length > 0
|
|
113
|
+
const evidenceHasStrong = strongKeys.length > 0
|
|
114
|
+
if (currentHasStrong && evidenceHasStrong) return false
|
|
115
|
+
|
|
116
|
+
const updatedAt = Date.parse(evidence.updatedAt || '')
|
|
117
|
+
if (!Number.isFinite(updatedAt) || now - updatedAt > WEAK_KEY_TTL_MS) return false
|
|
118
|
+
return intersects(snapshot.weakKeys, weakKeys)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Try to claim the current Codex closeout so Stop and native notify handle one turn only once.
|
|
123
|
+
*/
|
|
124
|
+
export function beginCodexCloseoutClaim(cwd, { payload = {}, turnState = null, source = '' } = {}) {
|
|
125
|
+
const snapshot = buildCodexCloseoutSnapshot({ payload, turnState })
|
|
126
|
+
const lockPath = getRuntimeEvidencePath(cwd, CODEX_CLOSEOUT_LOCK_FILE, { payload })
|
|
127
|
+
const evidencePath = getRuntimeEvidencePath(cwd, CODEX_CLOSEOUT_EVIDENCE_FILE, { payload })
|
|
128
|
+
const now = Date.now()
|
|
129
|
+
const lockPayload = {
|
|
130
|
+
source,
|
|
131
|
+
pid: process.pid,
|
|
132
|
+
createdAt: new Date(now).toISOString(),
|
|
133
|
+
turnId: snapshot.turnId,
|
|
134
|
+
sessionId: snapshot.sessionId,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
writeLockFile(lockPath, lockPayload)
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error?.code === 'EEXIST' && isLockStale(lockPath, now)) {
|
|
141
|
+
releaseLockFile(lockPath)
|
|
142
|
+
try {
|
|
143
|
+
writeLockFile(lockPath, lockPayload)
|
|
144
|
+
} catch (retryError) {
|
|
145
|
+
if (retryError?.code !== 'EEXIST') throw retryError
|
|
146
|
+
}
|
|
147
|
+
} else if (error?.code !== 'EEXIST') {
|
|
148
|
+
throw error
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const lockOwner = readLockPayload(lockPath)
|
|
153
|
+
if (
|
|
154
|
+
!lockOwner
|
|
155
|
+
|| lockOwner.createdAt !== lockPayload.createdAt
|
|
156
|
+
|| lockOwner.source !== source
|
|
157
|
+
|| lockOwner.pid !== process.pid
|
|
158
|
+
) {
|
|
159
|
+
return {
|
|
160
|
+
claimed: false,
|
|
161
|
+
reason: 'busy',
|
|
162
|
+
snapshot,
|
|
163
|
+
evidencePath,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const evidence = readRuntimeEvidence(cwd, CODEX_CLOSEOUT_EVIDENCE_FILE, { payload })
|
|
168
|
+
if (matchesCodexCloseoutEvidence(evidence, snapshot, now)) {
|
|
169
|
+
releaseLockFile(lockPath)
|
|
170
|
+
return {
|
|
171
|
+
claimed: false,
|
|
172
|
+
reason: 'duplicate',
|
|
173
|
+
snapshot,
|
|
174
|
+
evidencePath,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
claimed: true,
|
|
180
|
+
cwd,
|
|
181
|
+
payload,
|
|
182
|
+
source,
|
|
183
|
+
lockPath,
|
|
184
|
+
evidencePath,
|
|
185
|
+
snapshot,
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Persist the handled closeout fingerprint and release the in-flight lock.
|
|
191
|
+
*/
|
|
192
|
+
export function finalizeCodexCloseoutClaim(claim, meta = {}) {
|
|
193
|
+
if (!claim?.claimed) return
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
if (meta.handled !== false) {
|
|
197
|
+
writeRuntimeEvidence(claim.cwd, CODEX_CLOSEOUT_EVIDENCE_FILE, {
|
|
198
|
+
version: 2,
|
|
199
|
+
updatedAt: new Date().toISOString(),
|
|
200
|
+
source: meta.source || claim.source || '',
|
|
201
|
+
turnKind: meta.turnKind || '',
|
|
202
|
+
event: meta.event || '',
|
|
203
|
+
turnId: claim.snapshot.turnId,
|
|
204
|
+
sessionId: claim.snapshot.sessionId,
|
|
205
|
+
messageHash: claim.snapshot.messageHash,
|
|
206
|
+
strongKeys: claim.snapshot.strongKeys,
|
|
207
|
+
weakKeys: claim.snapshot.weakKeys,
|
|
208
|
+
}, { payload: claim.payload })
|
|
209
|
+
}
|
|
210
|
+
} finally {
|
|
211
|
+
releaseLockFile(claim.lockPath)
|
|
212
|
+
}
|
|
213
|
+
}
|
package/scripts/notify.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { readFileSync } from 'node:fs';
|
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { playSound as _playSound, desktopNotify as _desktopNotify } from './notify-ui.mjs';
|
|
10
|
+
import { beginCodexCloseoutClaim, finalizeCodexCloseoutClaim } from './notify-closeout.mjs';
|
|
10
11
|
import { resolveNotificationSource } from './notify-source.mjs';
|
|
11
12
|
import { buildCompactionContext, buildInjectContext, buildRouteInstruction, buildSemanticRouteInstruction, resolveCanonicalCommandSkill } from './notify-context.mjs';
|
|
12
13
|
import { resolveNotifyHost, shouldIgnoreCodexNotifyClient } from './notify-events.mjs';
|
|
@@ -16,7 +17,6 @@ import { cleanupProjectSessions } from './project-session-cleanup.mjs';
|
|
|
16
17
|
import { handleRouteCommand, resolveBootstrapFile } from './notify-route.mjs';
|
|
17
18
|
import { readSettings, readStdinJson, output, suppressedOutput, emptySuppress } from './notify-shared.mjs';
|
|
18
19
|
import { clearRouteContext, getApplicableRouteContext, writeRouteContext } from './runtime-context.mjs';
|
|
19
|
-
import { readRuntimeEvidence, writeRuntimeEvidence } from './runtime-artifacts.mjs';
|
|
20
20
|
import { appendReplayEvent, startReplaySession } from './replay-state.mjs';
|
|
21
21
|
import { clearTurnState, readTurnState } from './turn-state.mjs';
|
|
22
22
|
import { getWorkflowRecommendation } from './workflow-state.mjs';
|
|
@@ -38,7 +38,6 @@ const EVENT_NAME = {
|
|
|
38
38
|
PreCompact: IS_GEMINI ? 'BeforeAgent' : 'PreCompact',
|
|
39
39
|
};
|
|
40
40
|
const RALPH_LOOP_ROUTE_COMMANDS = new Set(['verify', 'loop']);
|
|
41
|
-
const CODEX_NATIVE_STOP_FILE = 'codex-native-stop.json';
|
|
42
41
|
|
|
43
42
|
const playSound = (event) => _playSound(PKG_ROOT, event);
|
|
44
43
|
const desktopNotify = (event, extra) => _desktopNotify(PKG_ROOT, event, extra);
|
|
@@ -140,27 +139,6 @@ function attachTurnSession(payload = {}, cwd = payload.cwd || process.cwd()) {
|
|
|
140
139
|
return { ...payload, sessionId };
|
|
141
140
|
}
|
|
142
141
|
|
|
143
|
-
function getCodexTurnId(payload = {}) {
|
|
144
|
-
return String(payload.turnId || payload.turn_id || payload['turn-id'] || '').trim();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function markCodexNativeStopProcessed(cwd, payload = {}) {
|
|
148
|
-
if (!IS_CODEX) return;
|
|
149
|
-
const turnId = getCodexTurnId(payload);
|
|
150
|
-
if (!turnId) return;
|
|
151
|
-
writeRuntimeEvidence(cwd, CODEX_NATIVE_STOP_FILE, {
|
|
152
|
-
turnId,
|
|
153
|
-
updatedAt: new Date().toISOString(),
|
|
154
|
-
}, { payload });
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function hasCodexNativeStopProcessed(cwd, payload = {}) {
|
|
158
|
-
const turnId = getCodexTurnId(payload);
|
|
159
|
-
if (!turnId) return false;
|
|
160
|
-
const evidence = readRuntimeEvidence(cwd, CODEX_NATIVE_STOP_FILE, { payload });
|
|
161
|
-
return evidence?.turnId === turnId;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
142
|
function readMainTurnState(cwd, payload = {}) {
|
|
165
143
|
const turnState = readTurnState(cwd, { payload });
|
|
166
144
|
return turnState?.role === 'main' ? turnState : null;
|
|
@@ -175,6 +153,43 @@ function shouldProcessCloseout(turnState) {
|
|
|
175
153
|
return false;
|
|
176
154
|
}
|
|
177
155
|
|
|
156
|
+
function processTurnCloseout(payload, turnPayload, turnState, settings = getSettings()) {
|
|
157
|
+
const cwd = turnPayload.cwd || process.cwd();
|
|
158
|
+
|
|
159
|
+
if (runTurnStopGate(turnPayload)) {
|
|
160
|
+
if (turnState && turnState.kind !== 'complete') consumeMainTurnState(cwd, turnState, turnPayload);
|
|
161
|
+
return { blocked: true };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!turnState) {
|
|
165
|
+
notifyByLevel('complete', buildNotifyExtra(turnPayload), settings);
|
|
166
|
+
clearRouteContext({ cwd, payload: turnPayload });
|
|
167
|
+
return { blocked: false };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (turnState.kind !== 'complete') {
|
|
171
|
+
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
172
|
+
clearRouteContext({ cwd, payload: turnPayload });
|
|
173
|
+
return { blocked: false };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (runRalphLoop(turnPayload, { turnState })) {
|
|
177
|
+
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
178
|
+
notifyByLevel('warning', buildNotifyExtra(payload), settings);
|
|
179
|
+
return { blocked: true };
|
|
180
|
+
}
|
|
181
|
+
if (runDeliveryGate(turnPayload)) {
|
|
182
|
+
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
183
|
+
notifyByLevel('warning', buildNotifyExtra(payload), settings);
|
|
184
|
+
return { blocked: true };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
notifyByLevel('complete', buildNotifyExtra(payload), settings);
|
|
188
|
+
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
189
|
+
clearRouteContext({ cwd, payload: turnPayload });
|
|
190
|
+
return { blocked: false };
|
|
191
|
+
}
|
|
192
|
+
|
|
178
193
|
function cmdPreCompact() {
|
|
179
194
|
const payload = readPayloadFromStdin();
|
|
180
195
|
const cwd = payload.cwd || process.cwd();
|
|
@@ -275,37 +290,29 @@ function cmdStop() {
|
|
|
275
290
|
const payload = readPayloadFromStdin();
|
|
276
291
|
const cwd = payload.cwd || process.cwd();
|
|
277
292
|
const turnPayload = attachTurnSession(payload, cwd);
|
|
278
|
-
if (IS_CODEX && hasCodexNativeStopProcessed(cwd, turnPayload)) {
|
|
279
|
-
emptySuppress();
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
293
|
const turnState = readMainTurnState(cwd, turnPayload);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const shouldProcess = shouldProcessCloseout(turnState);
|
|
289
|
-
if (shouldProcess && runRalphLoop(turnPayload, { turnState })) {
|
|
290
|
-
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
291
|
-
notifyByLevel('warning', buildNotifyExtra(payload));
|
|
292
|
-
markCodexNativeStopProcessed(cwd, turnPayload);
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
if (shouldProcess && runDeliveryGate(turnPayload)) {
|
|
296
|
-
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
297
|
-
notifyByLevel('warning', buildNotifyExtra(payload));
|
|
298
|
-
markCodexNativeStopProcessed(cwd, turnPayload);
|
|
294
|
+
const closeoutClaim = IS_CODEX
|
|
295
|
+
? beginCodexCloseoutClaim(cwd, { payload: turnPayload, turnState, source: 'stop' })
|
|
296
|
+
: null;
|
|
297
|
+
if (IS_CODEX && !closeoutClaim?.claimed) {
|
|
298
|
+
emptySuppress();
|
|
299
299
|
return;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
302
|
+
let handled = false;
|
|
303
|
+
let result = { blocked: false };
|
|
304
|
+
try {
|
|
305
|
+
result = processTurnCloseout(payload, turnPayload, turnState, getSettings());
|
|
306
|
+
handled = true;
|
|
307
|
+
} finally {
|
|
308
|
+
finalizeCodexCloseoutClaim(closeoutClaim, {
|
|
309
|
+
handled,
|
|
310
|
+
source: 'stop',
|
|
311
|
+
event: 'stop',
|
|
312
|
+
turnKind: turnState?.kind || '',
|
|
313
|
+
});
|
|
305
314
|
}
|
|
306
|
-
|
|
307
|
-
clearRouteContext({ cwd, payload: turnPayload });
|
|
308
|
-
markCodexNativeStopProcessed(cwd, turnPayload);
|
|
315
|
+
if (result.blocked) return;
|
|
309
316
|
emptySuppress();
|
|
310
317
|
}
|
|
311
318
|
|
|
@@ -333,39 +340,27 @@ function cmdCodexNotify() {
|
|
|
333
340
|
return;
|
|
334
341
|
}
|
|
335
342
|
if (type !== 'agent-turn-complete') return;
|
|
336
|
-
if (hasCodexNativeStopProcessed(cwd, turnPayload)) return;
|
|
337
343
|
|
|
338
344
|
const turnState = readMainTurnState(cwd, turnPayload);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
clearRouteContext({ cwd, payload: turnPayload });
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
if (turnState.kind !== 'complete') {
|
|
349
|
-
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
350
|
-
clearRouteContext({ cwd, payload: turnPayload });
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
345
|
+
const closeoutClaim = beginCodexCloseoutClaim(cwd, {
|
|
346
|
+
payload: turnPayload,
|
|
347
|
+
turnState,
|
|
348
|
+
source: 'codex-notify',
|
|
349
|
+
});
|
|
350
|
+
if (!closeoutClaim.claimed) return;
|
|
353
351
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
352
|
+
let handled = false;
|
|
353
|
+
try {
|
|
354
|
+
processTurnCloseout(data, turnPayload, turnState, getSettings());
|
|
355
|
+
handled = true;
|
|
356
|
+
} finally {
|
|
357
|
+
finalizeCodexCloseoutClaim(closeoutClaim, {
|
|
358
|
+
handled,
|
|
359
|
+
source: 'codex-notify',
|
|
360
|
+
event: type,
|
|
361
|
+
turnKind: turnState?.kind || '',
|
|
362
|
+
});
|
|
364
363
|
}
|
|
365
|
-
|
|
366
|
-
notifyByLevel('complete', buildNotifyExtra(data), settings);
|
|
367
|
-
consumeMainTurnState(cwd, turnState, turnPayload);
|
|
368
|
-
clearRouteContext({ cwd, payload: turnPayload });
|
|
369
364
|
}
|
|
370
365
|
|
|
371
366
|
switch (cmd) {
|