@principles/host-runtime 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.
@@ -342,9 +342,9 @@ function insertCorrectionPain({ db, candidate, workspaceDir, painId, detection,
342
342
  // is NOT used here: buildEvidence already truncated it upstream, so a token
343
343
  // straddling that cut would survive sanitization the same way.
344
344
  const excerpt = sanitizeString(candidate.text, workspaceDir);
345
- db.prepare(`INSERT INTO pain_events (session_id, source, score, reason, severity, origin, confidence, text, canonical_pain_id, runtime_task_id, created_at)
346
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
347
- .run(candidate.rootSessionId, 'user_correction', GOVERNANCE_STRONG_PAIN_SCORE, reason.slice(0, MAX_REASON_BOUND), 'severe', 'system_infer', null, excerpt, painId, null, nowIso);
345
+ db.prepare(`INSERT INTO pain_events (session_id, source, score, reason, severity, origin, confidence, text, canonical_pain_id, runtime_task_id, host_kind, created_at)
346
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
347
+ .run(candidate.rootSessionId, 'user_correction', GOVERNANCE_STRONG_PAIN_SCORE, reason.slice(0, MAX_REASON_BOUND), 'severe', 'system_infer', null, excerpt, painId, null, candidate.hostKind, nowIso);
348
348
  return reason;
349
349
  }
350
350
  function taskSubmissionPayload(candidate, workspaceDir, reason) {
@@ -415,6 +415,9 @@ function admitCorrection({ db, candidate, workspaceDir, now, nowIso, keywordStor
415
415
  // Live + transcript duplicate or a crash-replay: the canonical pain
416
416
  // already exists — no second row, no second quota consumption.
417
417
  duplicate = true;
418
+ // PRI-640 §17: enrich unknown → codex from proven governance lineage;
419
+ // the IS NULL guard keeps any durable attribution (never overwrites).
420
+ db.prepare('UPDATE pain_events SET host_kind = ? WHERE canonical_pain_id = ? AND host_kind IS NULL').run(candidate.hostKind, painId);
418
421
  }
419
422
  else {
420
423
  if (!tryConsumeRateLimit({ db, rootSessionId: candidate.rootSessionId, ruleVersion: detection.ruleVersion, now })) {
@@ -488,9 +491,12 @@ function admitToolFailure({ db, candidate, workspaceDir, nowIso }) {
488
491
  db.prepare(`INSERT INTO tool_calls (session_id, tool_name, outcome, duration_ms, exit_code, error_type, error_message, gfi_before, gfi_after, params_json, result_preview, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
489
492
  .run(candidate.rootSessionId, candidate.toolName, outcome.failure ? 'failure' : 'success', outcome.durationMs ?? null, outcome.exitCode, outcome.error ? outcome.error.split(/[\s:]/, 1)[0] : null, outcome.error ?? null, null, null, paramsJson, resultPreview, nowIso);
490
493
  reason = `tool=${candidate.toolName}; error=${outcome.error ?? `exit=${outcome.exitCode}`}; path=${relativePath}`;
491
- db.prepare(`INSERT INTO pain_events (session_id, source, score, reason, severity, origin, confidence, text, canonical_pain_id, runtime_task_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
492
- .run(candidate.rootSessionId, sourceObservation.failureSource ?? 'tool_failure', painScore, reason, painScore >= 70 ? 'severe' : painScore >= 40 ? 'moderate' : 'mild', 'system_infer', null, null, painId, null, nowIso);
494
+ db.prepare(`INSERT INTO pain_events (session_id, source, score, reason, severity, origin, confidence, text, canonical_pain_id, runtime_task_id, host_kind, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
495
+ .run(candidate.rootSessionId, sourceObservation.failureSource ?? 'tool_failure', painScore, reason, painScore >= 70 ? 'severe' : painScore >= 40 ? 'moderate' : 'mild', 'system_infer', null, null, painId, null, candidate.hostKind, nowIso);
493
496
  }
497
+ // PRI-640 §17: enrich unknown → codex on the already-admitted duplicate
498
+ // pain row; the IS NULL guard keeps any durable attribution.
499
+ db.prepare('UPDATE pain_events SET host_kind = ? WHERE canonical_pain_id = ? AND host_kind IS NULL').run(candidate.hostKind, painId);
494
500
  if (!duplicateWithoutPain) {
495
501
  insertAdmissionMarker({ db, candidate, workspaceDir, canonicalPainId: painId, ruleVersion: null, reason: sanitizeString(reason, workspaceDir), nowIso });
496
502
  }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ import { buildActivePrinciplePromptContext } from './active-principle-prompt.js'
3
3
  import { type RuleContextProvider, type RuleInputEnrichmentProvider } from './production-rulehost-gate.js';
4
4
  import type { RuleImplementationRuntime } from './rule-implementation-runtime.js';
5
5
  import { type PainDatabaseFactory, type PainEnrichmentProvider } from './production-pain-evidence.js';
6
+ import type { GovernanceHostKind } from '@principles/core/runtime-v2';
6
7
  export * from './active-principle-prompt.js';
7
8
  export * from './pd-config.js';
8
9
  export * from './production-rulehost-gate.js';
@@ -53,4 +54,6 @@ export declare function createProductionHostRuntime(options?: Partial<Pick<HostR
53
54
  ruleImplementationRuntime?: RuleImplementationRuntime;
54
55
  painEnrichmentProvider?: PainEnrichmentProvider;
55
56
  painDatabaseFactory?: PainDatabaseFactory;
57
+ /** PRI-640: host attribution supplied by the constructing host adapter (OpenClaw / Codex). */
58
+ hostKind?: GovernanceHostKind;
56
59
  }): HostRuntime;
package/dist/index.js CHANGED
@@ -146,6 +146,7 @@ export function createProductionHostRuntime(options = {}) {
146
146
  afterToolCall: options.afterToolCall ?? createProductionPainEvidenceHandler({
147
147
  ...(options.painEnrichmentProvider ? { painEnrichmentProvider: options.painEnrichmentProvider } : {}),
148
148
  ...(options.painDatabaseFactory ? { painDatabaseFactory: options.painDatabaseFactory } : {}),
149
+ ...(options.hostKind ? { hostKind: options.hostKind } : {}),
149
150
  }),
150
151
  beforeToolCall: options.beforeToolCall ?? productionGate,
151
152
  async beforePromptBuild(event) {
@@ -170,6 +170,20 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
170
170
  }
171
171
  const runtimeConfigResult = resolveRuntimeConfigFromPdConfig(configResult.effective, (name) => envGetter(name));
172
172
  if (isRuntimeConfigError(runtimeConfigResult)) {
173
+ // PRI-638 P1-C: the shared runtime resolver reads the Diagnostician
174
+ // binding; an Owner-disabled agent reports reason='disabled'. That is an
175
+ // intentional governance pause, not a broken runtime — report it
176
+ // distinctly so consumers (Codex worker) never treat it as degradation.
177
+ if (runtimeConfigResult.reason === 'disabled') {
178
+ const disabledInfo = JSON.stringify({
179
+ reason: 'capability_disabled',
180
+ message: runtimeConfigResult.message,
181
+ nextAction: runtimeConfigResult.nextAction,
182
+ });
183
+ emitEvent('INTERNALIZATION_CONSUMER_SKIP', disabledInfo);
184
+ logger.warn(`[PD:${logLabel}] Cycle skipped: Diagnostician capability disabled — ${runtimeConfigResult.message}`);
185
+ return { ran: false, skipReason: 'capability_disabled' };
186
+ }
173
187
  const rtInfo = JSON.stringify({
174
188
  reason: 'runtime_config_error',
175
189
  message: runtimeConfigResult.message,
@@ -1,4 +1,5 @@
1
1
  import Database from 'better-sqlite3';
2
+ import type { GovernanceHostKind } from '@principles/core/runtime-v2';
2
3
  import type { HostEvent, HostEventResult } from '@principles/core/host';
3
4
  /** Shared with the governance admission path so both gate tool failures identically. */
4
5
  export declare const PRODUCTION_WRITE_TOOLS: ReadonlySet<string>;
@@ -75,9 +76,15 @@ export declare function deriveProductionCorrectionPainIdentity(fields: {
75
76
  painId: string;
76
77
  };
77
78
  declare function hasCanonicalSchema(db: Database.Database): boolean;
79
+ /**
80
+ * PRI-640: host attribution is supplied by the host adapter that constructs
81
+ * this shared handler (OpenClaw / Codex adapters) — never guessed inside the
82
+ * writer. Omitted -> NULL (unknown).
83
+ */
78
84
  export declare function createProductionPainEvidenceHandler(options?: {
79
85
  painEnrichmentProvider?: PainEnrichmentProvider;
80
86
  painDatabaseFactory?: PainDatabaseFactory;
87
+ hostKind?: GovernanceHostKind;
81
88
  }): (event: HostEvent) => Promise<HostEventResult>;
82
89
  export declare function resetProductionPainCooldownForTest(): void;
83
90
  export declare function productionPainCooldownEntryCountForTest(): number;
@@ -173,7 +173,7 @@ export function deriveProductionCorrectionPainIdentity(fields) {
173
173
  const REQUIRED_COLUMNS = {
174
174
  sessions: { session_id: 'TEXT', started_at: 'TEXT', updated_at: 'TEXT' },
175
175
  tool_calls: { session_id: 'TEXT', tool_name: 'TEXT', outcome: 'TEXT', duration_ms: 'INTEGER', exit_code: 'INTEGER', error_type: 'TEXT', error_message: 'TEXT', gfi_before: 'REAL', gfi_after: 'REAL', params_json: 'TEXT', result_preview: 'TEXT', created_at: 'TEXT' },
176
- pain_events: { session_id: 'TEXT', source: 'TEXT', score: 'REAL', reason: 'TEXT', severity: 'TEXT', origin: 'TEXT', confidence: 'REAL', text: 'TEXT', canonical_pain_id: 'TEXT', runtime_task_id: 'TEXT', created_at: 'TEXT' },
176
+ pain_events: { session_id: 'TEXT', source: 'TEXT', score: 'REAL', reason: 'TEXT', severity: 'TEXT', origin: 'TEXT', confidence: 'REAL', text: 'TEXT', canonical_pain_id: 'TEXT', runtime_task_id: 'TEXT', host_kind: 'TEXT', created_at: 'TEXT' },
177
177
  };
178
178
  function pragmaField(row, key) {
179
179
  return isRecord(row) && Object.hasOwn(row, key) ? Object.getOwnPropertyDescriptor(row, key)?.value : undefined;
@@ -224,9 +224,14 @@ function hasCanonicalSchema(db) {
224
224
  db.prepare('SELECT 1 FROM pain_events WHERE canonical_pain_id = ?');
225
225
  db.prepare('INSERT INTO sessions (session_id, started_at, updated_at) VALUES (?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET updated_at = excluded.updated_at');
226
226
  db.prepare('INSERT INTO tool_calls (session_id, tool_name, outcome, duration_ms, exit_code, error_type, error_message, gfi_before, gfi_after, params_json, result_preview, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
227
- db.prepare('INSERT INTO pain_events (session_id, source, score, reason, severity, origin, confidence, text, canonical_pain_id, runtime_task_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
227
+ db.prepare('INSERT INTO pain_events (session_id, source, score, reason, severity, origin, confidence, text, canonical_pain_id, runtime_task_id, host_kind, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
228
228
  return true;
229
229
  }
230
+ /**
231
+ * PRI-640: host attribution is supplied by the host adapter that constructs
232
+ * this shared handler (OpenClaw / Codex adapters) — never guessed inside the
233
+ * writer. Omitted -> NULL (unknown).
234
+ */
230
235
  export function createProductionPainEvidenceHandler(options = {}) {
231
236
  return async (event) => {
232
237
  const dbPath = path.join(event.context.workspaceDir, '.state', 'trajectory.db');
@@ -319,8 +324,8 @@ export function createProductionPainEvidenceHandler(options = {}) {
319
324
  // declared enum (event-types.ts: who reported the pain), not an
320
325
  // attribution claim.
321
326
  const reason = `tool=${toolName}; error=${outcome.error ?? `exit=${outcome.exitCode}`}; path=${relativePath}`;
322
- db.prepare(`INSERT INTO pain_events (session_id, source, score, reason, severity, origin, confidence, text, canonical_pain_id, runtime_task_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
323
- .run(event.context.sessionId, sourceObservation.failureSource ?? 'tool_failure', painScore, reason, painScore >= 70 ? 'severe' : painScore >= 40 ? 'moderate' : 'mild', 'system_infer', null, enrichment.evidence?.map((entry) => `${entry.sourceRef}: ${entry.note}`).join('\n') ?? null, painId, null, createdAt);
327
+ db.prepare(`INSERT INTO pain_events (session_id, source, score, reason, severity, origin, confidence, text, canonical_pain_id, runtime_task_id, host_kind, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
328
+ .run(event.context.sessionId, sourceObservation.failureSource ?? 'tool_failure', painScore, reason, painScore >= 70 ? 'severe' : painScore >= 40 ? 'moderate' : 'mild', 'system_infer', null, enrichment.evidence?.map((entry) => `${entry.sourceRef}: ${entry.note}`).join('\n') ?? null, painId, null, options.hostKind ?? null, createdAt);
324
329
  }
325
330
  })();
326
331
  if (admitted && !duplicate) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/host-runtime",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Shared host-neutral orchestration for Principles Disciple MVP-Core hook paths.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",