@principles/codex-adapter 0.2.0 → 0.2.1
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/dist/pd-hook.js
CHANGED
|
@@ -109,7 +109,7 @@ export async function processHookInvocation(rawStdin, _env = process.env, cwd =
|
|
|
109
109
|
}
|
|
110
110
|
try {
|
|
111
111
|
if (event.kind === 'session_start') {
|
|
112
|
-
const health = await createProductionHostRuntime().health(resolution.workspaceDir);
|
|
112
|
+
const health = await createProductionHostRuntime({ hostKind: 'codex' }).health(resolution.workspaceDir);
|
|
113
113
|
if (!health.ok)
|
|
114
114
|
return { stdout: {}, exitCode: 0, stderr: [diagnostic(health.reason ?? 'runtime_unhealthy', health.nextAction ?? 'Inspect the Workspace runtime.')] };
|
|
115
115
|
return { stdout: adapter.encodeOutput({ decision: 'allow', source: event.source }, 'session_start'), exitCode: 0, stderr: [] };
|
|
@@ -117,7 +117,7 @@ export async function processHookInvocation(rawStdin, _env = process.env, cwd =
|
|
|
117
117
|
const ingestionDiagnostics = ingestionEnabled
|
|
118
118
|
? await runConversationIngestion({ rawPayload: parsed, kind: event.kind, workspaceDir: resolution.workspaceDir, env: _env })
|
|
119
119
|
: [];
|
|
120
|
-
const result = await createProductionHostRuntime().dispatch(event);
|
|
120
|
+
const result = await createProductionHostRuntime({ hostKind: 'codex' }).dispatch(event);
|
|
121
121
|
const stderr = [...(result.warnings ?? []).slice(0, 16).map((warning) => diagnostic(warning, 'Inspect PD Workspace state and retry; the hook failed open.')), ...ingestionDiagnostics];
|
|
122
122
|
return { stdout: adapter.encodeOutput(result, event.kind), exitCode: 0, stderr };
|
|
123
123
|
}
|
|
@@ -11,6 +11,8 @@ export interface CodexWorkerCycleStepReport {
|
|
|
11
11
|
readonly status: 'succeeded' | 'failed' | 'retried' | 'skipped' | 'degraded';
|
|
12
12
|
readonly message?: string;
|
|
13
13
|
readonly errorCategory?: string;
|
|
14
|
+
/** PRI-638 P1-C: recovery action for capability-disabled pauses. */
|
|
15
|
+
readonly nextAction?: string;
|
|
14
16
|
} | null;
|
|
15
17
|
readonly downstream: InternalizationConsumerCycleOutcome | null;
|
|
16
18
|
}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import fs from 'node:fs';
|
|
25
25
|
import path from 'node:path';
|
|
26
|
-
import { computeFeatureFlagsFromConfig, createRuntimeStateHandle, createPainSignalBridge, isRetryWaitBackoffElapsed, PrincipleTreeLedgerAdapter, } from '@principles/core/runtime-v2';
|
|
26
|
+
import { computeFeatureFlagsFromConfig, createRuntimeStateHandle, createPainSignalBridge, isRetryWaitBackoffElapsed, PrincipleTreeLedgerAdapter, resolveDiagnosticianCapability, } from '@principles/core/runtime-v2';
|
|
27
27
|
import { loadPdConfigForPlugin, loadFeatureFlagFromConfig, reconcileGovernanceContinuation, runInternalizationConsumerCycle, } from '@principles/host-runtime';
|
|
28
28
|
import { catchUpCodexIngestion } from '../ingestion/catch-up.js';
|
|
29
29
|
const WORKER_OWNER = 'companion-worker';
|
|
@@ -86,6 +86,15 @@ export async function runCodexWorkspaceWorkerCycle(options) {
|
|
|
86
86
|
return { ...base, mode: 'paused', reason: 'host.codex_disabled', nextAction: 'Set features.host.codex.enabled=true in the Workspace .pd/config.yaml to enable Codex PD behavior.' };
|
|
87
87
|
}
|
|
88
88
|
const consumerFlag = loadFeatureFlagFromConfig(workspaceDir, 'internalization_auto_consumer', { info: (m) => logger.info(m), warn: (m) => logger.warn(m) });
|
|
89
|
+
// PRI-638 P1-C: the Diagnostician capability gate is resolved from the same
|
|
90
|
+
// canonical authority the runtime factory uses. An Owner-disabled agent is
|
|
91
|
+
// an intentional governance state: the worker must NOT pick pending tasks,
|
|
92
|
+
// must NOT invoke any runner (provider calls = 0), must NOT report the
|
|
93
|
+
// workspace degraded, and must keep catch-up / reconciliation / downstream
|
|
94
|
+
// running. Pending tasks stay untouched and resume on a later cycle after
|
|
95
|
+
// the Owner re-enables the agent.
|
|
96
|
+
const capability = resolveDiagnosticianCapability(config.effective);
|
|
97
|
+
const diagnosticianDisabled = !capability.available;
|
|
89
98
|
// Step 2 — Catch-up transcript lag (SPEC §13: gated by the ingestion flag,
|
|
90
99
|
// NOT by the consumer flag; catchUpCodexIngestion re-checks and returns a
|
|
91
100
|
// zero-I/O skip when ingestion is off).
|
|
@@ -99,7 +108,18 @@ export async function runCodexWorkspaceWorkerCycle(options) {
|
|
|
99
108
|
const reconcile = await reconcileGovernanceContinuation({ workspaceDir });
|
|
100
109
|
// Steps 4–6 — execution authority: internalization_auto_consumer.
|
|
101
110
|
let diagnostician = null;
|
|
102
|
-
if (
|
|
111
|
+
if (diagnosticianDisabled) {
|
|
112
|
+
// PRI-638 P1-C: intentional governance pause. No candidate picking, no
|
|
113
|
+
// provider call, no degraded mode. The rest of the cycle (downstream)
|
|
114
|
+
// continues below; pending tasks resume once the Owner re-enables.
|
|
115
|
+
diagnostician = {
|
|
116
|
+
taskId: '',
|
|
117
|
+
status: 'skipped',
|
|
118
|
+
message: 'capability_disabled',
|
|
119
|
+
nextAction: capability.available ? undefined : capability.nextAction,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
else if (!consumerFlag.enabled) {
|
|
103
123
|
// paused: execution pause, NOT evidence freeze. Catch-up + reconcile
|
|
104
124
|
// already ran above; manual CLI remains allowed (SPEC §13).
|
|
105
125
|
return {
|
|
@@ -110,63 +130,68 @@ export async function runCodexWorkspaceWorkerCycle(options) {
|
|
|
110
130
|
report: { catchUp, reconcile, diagnostician: null, downstream: null },
|
|
111
131
|
};
|
|
112
132
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
133
|
+
else {
|
|
134
|
+
// Step 4 — expired-lease recovery sweep, then Step 5/6: at most one
|
|
135
|
+
// Diagnostician task via the existing bridge lease/runner contract.
|
|
136
|
+
// (Reached only when the Diagnostician capability is enabled AND the
|
|
137
|
+
// consumer flag is on — an intentional capability pause never reaches
|
|
138
|
+
// candidate picking or any provider call.)
|
|
117
139
|
try {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
workspaceDir,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
taskId: candidate.taskId
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
140
|
+
const handle = await createRuntimeStateHandle({ workspaceDir, readonly: false });
|
|
141
|
+
try {
|
|
142
|
+
const sweep = await handle.stateManager.runRecoverySweep();
|
|
143
|
+
if (sweep.recovered > 0 || sweep.errors.length > 0) {
|
|
144
|
+
emitEvent('CODEX_WORKER_RECOVERY_SWEEP', JSON.stringify({ recovered: sweep.recovered, failed: sweep.errors.length }));
|
|
145
|
+
}
|
|
146
|
+
const candidate = await pickDiagnosticianCandidate(handle.stateManager, options.diagnosticianCandidateLimit ?? DEFAULT_DIAG_CANDIDATE_LIMIT);
|
|
147
|
+
if (candidate !== null) {
|
|
148
|
+
const stateDir = path.join(workspaceDir, '.state');
|
|
149
|
+
const bridge = await createPainSignalBridge({
|
|
150
|
+
workspaceDir,
|
|
151
|
+
stateDir,
|
|
152
|
+
ledgerAdapter: new PrincipleTreeLedgerAdapter({ stateDir }),
|
|
153
|
+
owner: WORKER_OWNER,
|
|
154
|
+
effectiveConfig: config.effective,
|
|
155
|
+
getEnvVar: (name) => process.env[name],
|
|
156
|
+
});
|
|
157
|
+
const executed = await bridge.executePendingDiagnosis({ taskId: candidate.taskId });
|
|
158
|
+
diagnostician = {
|
|
159
|
+
taskId: candidate.taskId,
|
|
160
|
+
status: executed.status,
|
|
161
|
+
...(executed.message !== undefined ? { message: executed.message.slice(0, 200) } : {}),
|
|
162
|
+
...(executed.errorCategory !== undefined ? { errorCategory: executed.errorCategory } : {}),
|
|
163
|
+
};
|
|
164
|
+
// The bridge stays cached per workspace (bounded: one per workspace
|
|
165
|
+
// per worker process, exactly like the OpenClaw plugin host) — no
|
|
166
|
+
// per-cycle dispose, so a concurrent cycle can never hit a disposed
|
|
167
|
+
// bridge. The factory self-disposes losing concurrent constructions.
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// No eligible candidate, but a retry_wait task may still be inside
|
|
171
|
+
// its backoff window — report it so the cycle is observable instead
|
|
172
|
+
// of looking like "nothing pending" (review P1: head-of-line).
|
|
173
|
+
const waiting = await handle.stateManager.listTasks({ taskKind: 'diagnostician', status: 'retry_wait', orderBy: 'updated_at_asc', limit: 1 });
|
|
174
|
+
const [oldestWaiting] = waiting;
|
|
175
|
+
if (oldestWaiting !== undefined && !isRetryWaitBackoffElapsed(oldestWaiting.status, oldestWaiting.leaseExpiresAt)) {
|
|
176
|
+
diagnostician = { taskId: oldestWaiting.taskId, status: 'skipped', message: 'retry_wait_pending' };
|
|
177
|
+
}
|
|
153
178
|
}
|
|
154
179
|
}
|
|
180
|
+
finally {
|
|
181
|
+
await handle.close().catch(() => undefined);
|
|
182
|
+
}
|
|
155
183
|
}
|
|
156
|
-
|
|
157
|
-
|
|
184
|
+
catch (error) {
|
|
185
|
+
const detail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
|
|
186
|
+
return {
|
|
187
|
+
...base,
|
|
188
|
+
mode: 'degraded',
|
|
189
|
+
reason: `diagnostician_execution_failed:${detail}`,
|
|
190
|
+
nextAction: 'Inspect the Workspace runtime profile and provider configuration; the task keeps its pending/retry state and no evidence was mutated.',
|
|
191
|
+
report: { catchUp, reconcile, diagnostician: null, downstream: null },
|
|
192
|
+
};
|
|
158
193
|
}
|
|
159
194
|
}
|
|
160
|
-
catch (error) {
|
|
161
|
-
const detail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
|
|
162
|
-
return {
|
|
163
|
-
...base,
|
|
164
|
-
mode: 'degraded',
|
|
165
|
-
reason: `diagnostician_execution_failed:${detail}`,
|
|
166
|
-
nextAction: 'Inspect the Workspace runtime profile and provider configuration; the task keeps its pending/retry state and no evidence was mutated.',
|
|
167
|
-
report: { catchUp, reconcile, diagnostician: null, downstream: null },
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
195
|
// Step 7 — ONE bounded downstream consumer cycle via the shared executor
|
|
171
196
|
// (same implementation the OpenClaw auto-consumer runs).
|
|
172
197
|
const downstream = await runInternalizationConsumerCycle(workspaceDir, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principles/codex-adapter",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Codex CLI host adapter for Principles Disciple — implements HostAdapter interface for OpenAI Codex CLI's stdin/stdout JSON hook model (ADR-0020).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|