psyche-ai 11.8.0 → 11.9.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.
@@ -38,7 +38,10 @@ declare function stripPsycheTags(text: string): string;
38
38
  export interface ThrongletsSignalPayload {
39
39
  kind: "psyche_state";
40
40
  agent_id: string;
41
+ context: string;
41
42
  message: string;
43
+ model: "psyche";
44
+ session_id: string;
42
45
  }
43
46
  export interface PsycheClaudeSdkOptions {
44
47
  /** User ID for relationship tracking. Default: internal shared bucket (`_default`). */
@@ -34,6 +34,7 @@ import { serializeThrongletsExportAsTrace } from "../thronglets-runtime.js";
34
34
  import { resolveRelationshipUserId } from "../relationship-key.js";
35
35
  import { resolveAmbientPriorsForTurn, } from "../ambient-runtime.js";
36
36
  import { safeProcessInput, safeProcessOutput } from "./fail-open.js";
37
+ import { deriveThrongletsSpace } from "../thronglets-bridge.js";
37
38
  // ── Dimension description ────────────────────────────────────
38
39
  const DIM_THRESHOLDS = {
39
40
  high: 70,
@@ -118,7 +119,7 @@ export class PsycheClaudeSDK {
118
119
  thronglets: this.opts.ambient
119
120
  ? {
120
121
  ...this.opts.ambient,
121
- space: this.opts.ambient.space ?? "psyche",
122
+ space: this.opts.ambient.space ?? deriveThrongletsSpace(),
122
123
  }
123
124
  : undefined,
124
125
  });
@@ -182,6 +183,7 @@ export class PsycheClaudeSDK {
182
183
  const ambientPriors = await self.resolveAmbientPriors(userMessage, currentGoal, activePolicy, currentTurnCorrection);
183
184
  const result = await safeProcessInput(self.engine, userMessage, {
184
185
  userId: self.opts.userId,
186
+ sessionId: runtimeContext.sessionId ?? self.lastRuntimeContext.sessionId,
185
187
  ambientPriors,
186
188
  currentGoal,
187
189
  activePolicy,
@@ -212,6 +214,7 @@ export class PsycheClaudeSDK {
212
214
  async processResponse(text, opts) {
213
215
  const result = await safeProcessOutput(this.engine, text, {
214
216
  userId: this.opts.userId,
217
+ sessionId: this.resolveSessionId(),
215
218
  signals: opts?.signals,
216
219
  signalConfidence: opts?.signalConfidence,
217
220
  }, "claude-sdk.processOutput");
@@ -250,10 +253,14 @@ export class PsycheClaudeSDK {
250
253
  return null;
251
254
  const state = this.engine.getState();
252
255
  const s = state.current;
256
+ const sessionId = this.resolveSessionId();
253
257
  return {
254
258
  kind: "psyche_state",
255
259
  agent_id: this.resolveAgentId(),
260
+ context: `psyche:session:${sessionId}:user:${this.opts.userId}`,
256
261
  message: `order:${s.order} flow:${s.flow} boundary:${s.boundary} resonance:${s.resonance}`,
262
+ model: "psyche",
263
+ session_id: sessionId,
257
264
  };
258
265
  }
259
266
  /**
@@ -16,6 +16,7 @@ import { normalizeCurrentGoal, normalizeCurrentTurnCorrection, resolveRuntimeAct
16
16
  import { resolveAmbientPriorsForTurn, } from "../ambient-runtime.js";
17
17
  import { composePsycheContext, safeProcessInput, safeProcessOutput } from "./fail-open.js";
18
18
  import { coerceWritebackSignalInput } from "../writeback-signals.js";
19
+ import { deriveThrongletsSpace } from "../thronglets-bridge.js";
19
20
  /**
20
21
  * LangChain integration helper for PsycheEngine.
21
22
  *
@@ -59,7 +60,7 @@ export class PsycheLangChain {
59
60
  thronglets: ambient
60
61
  ? {
61
62
  ...(ambient === true ? {} : ambient),
62
- space: ambient === true ? "psyche" : (ambient.space ?? "psyche"),
63
+ space: ambient === true ? deriveThrongletsSpace() : (ambient.space ?? deriveThrongletsSpace()),
63
64
  }
64
65
  : undefined,
65
66
  });
@@ -1,5 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { PsycheEngine } from "../core.js";
3
+ import type { ProcessInputResult } from "../core.js";
3
4
  import { fetchAmbientPriorsFromThronglets, type ThrongletsAmbientRuntimeOptions } from "../ambient-runtime.js";
4
5
  import { type ActivePolicyRule, type AmbientPriorView, type CurrentGoal } from "../types.js";
5
6
  export interface McpAmbientRuntimeOptions {
@@ -7,6 +8,10 @@ export interface McpAmbientRuntimeOptions {
7
8
  thronglets?: ThrongletsAmbientRuntimeOptions;
8
9
  fetcher?: typeof fetchAmbientPriorsFromThronglets;
9
10
  }
11
+ export declare function resolveMcpTurnCacheSession(sessionId?: string): string;
12
+ export declare function cacheMcpTurnResult(result: ProcessInputResult, sessionId?: string): void;
13
+ export declare function getCachedMcpTurnResult(sessionId?: string): ProcessInputResult | null;
14
+ export declare function resetMcpTurnCacheForTests(): void;
10
15
  declare function getEngine(): Promise<PsycheEngine>;
11
16
  export declare function resolveRuntimeAmbientPriors(text: string, explicit?: AmbientPriorView[], currentGoal?: CurrentGoal, activePolicy?: ActivePolicyRule[], currentTurnCorrectionOrOpts?: string | McpAmbientRuntimeOptions, opts?: McpAmbientRuntimeOptions): Promise<AmbientPriorView[] | undefined>;
12
17
  declare const server: McpServer;
@@ -36,6 +36,7 @@ import { CURRENT_GOALS, normalizeCurrentTurnCorrection, resolveRuntimeActivePoli
36
36
  import { getPackageVersion } from "../update.js";
37
37
  import { runDemo } from "../demo.js";
38
38
  import { safeProcessInput, safeProcessOutput } from "./fail-open.js";
39
+ import { deriveThrongletsSpace } from "../thronglets-bridge.js";
39
40
  const PACKAGE_VERSION = await getPackageVersion();
40
41
  // ── Config from env ────────────────────────────────────────
41
42
  const MBTI = (process.env.PSYCHE_MBTI ?? "ENFP");
@@ -54,7 +55,7 @@ const DEFAULT_MCP_AMBIENT_OPTIONS = {
54
55
  thronglets: {
55
56
  binaryPath: process.env.THRONGLETS_BIN,
56
57
  dataDir: process.env.THRONGLETS_DATA_DIR,
57
- space: process.env.THRONGLETS_SPACE ?? "psyche",
58
+ space: process.env.THRONGLETS_SPACE ?? deriveThrongletsSpace(),
58
59
  },
59
60
  };
60
61
  const CURRENT_GOAL_SCHEMA = z.enum(CURRENT_GOALS);
@@ -96,9 +97,29 @@ function parseCLIArgs() {
96
97
  return overrides;
97
98
  }
98
99
  // ── Turn cache for on-demand resource access ──────────────
99
- // Single-client assumption: MCP stdio transport serves one host at a time.
100
- // If this ever becomes multi-client, turn cache must be keyed per session.
101
- let lastTurnResult = null;
100
+ // Hosts may overlap sessions on one MCP transport, so cache per session key.
101
+ const DEFAULT_TURN_CACHE_SESSION = "__default__";
102
+ const turnResultsBySession = new Map();
103
+ let lastTurnCacheSession = DEFAULT_TURN_CACHE_SESSION;
104
+ export function resolveMcpTurnCacheSession(sessionId) {
105
+ const normalized = sessionId?.trim();
106
+ return normalized ? normalized : DEFAULT_TURN_CACHE_SESSION;
107
+ }
108
+ export function cacheMcpTurnResult(result, sessionId) {
109
+ const key = resolveMcpTurnCacheSession(sessionId);
110
+ turnResultsBySession.set(key, result);
111
+ lastTurnCacheSession = key;
112
+ }
113
+ export function getCachedMcpTurnResult(sessionId) {
114
+ if (sessionId) {
115
+ return turnResultsBySession.get(resolveMcpTurnCacheSession(sessionId)) ?? null;
116
+ }
117
+ return turnResultsBySession.get(lastTurnCacheSession) ?? null;
118
+ }
119
+ export function resetMcpTurnCacheForTests() {
120
+ turnResultsBySession.clear();
121
+ lastTurnCacheSession = DEFAULT_TURN_CACHE_SESSION;
122
+ }
102
123
  // ── Engine singleton ───────────────────────────────────────
103
124
  let engine = null;
104
125
  async function getEngine() {
@@ -118,7 +139,7 @@ async function getEngine() {
118
139
  diagnostics: true,
119
140
  throngletsBridge: {
120
141
  dataDir: process.env.THRONGLETS_DATA_DIR,
121
- space: process.env.THRONGLETS_SPACE ?? "psyche",
142
+ space: process.env.THRONGLETS_SPACE ?? deriveThrongletsSpace(),
122
143
  },
123
144
  };
124
145
  const persist = cfg.persist !== false;
@@ -208,14 +229,17 @@ server.resource("state", "psyche://state", {
208
229
  }],
209
230
  };
210
231
  });
211
- // Helper: register a turn-scoped resource backed by lastTurnResult.
232
+ // Helper: register a turn-scoped resource backed by the per-session turn cache.
212
233
  // Returns empty JSON when no turn has been processed yet (fail-open).
213
234
  function turnResource(name, uri, description, pick) {
214
235
  server.resource(name, uri, { description, mimeType: "application/json" }, async (u) => ({
215
236
  contents: [{
216
237
  uri: u.href,
217
238
  mimeType: "application/json",
218
- text: lastTurnResult ? JSON.stringify(pick(lastTurnResult)) : "{}",
239
+ text: (() => {
240
+ const cached = getCachedMcpTurnResult(u.searchParams.get("session") ?? undefined);
241
+ return cached ? JSON.stringify(pick(cached)) : "{}";
242
+ })(),
219
243
  }],
220
244
  }));
221
245
  }
@@ -245,6 +269,7 @@ server.tool("process_input", "Process user input through the emotional engine. R
245
269
  "Call this BEFORE generating a response to the user.", {
246
270
  text: z.string().describe("The user's message text"),
247
271
  userId: z.string().optional().describe("Optional user ID for multi-user relationship tracking"),
272
+ sessionId: z.string().optional().describe("Optional session ID for overlapping host sessions and turn-scoped resource lookup"),
248
273
  ambientPriors: z.array(z.object({
249
274
  summary: z.string(),
250
275
  confidence: z.number().min(0).max(1),
@@ -262,19 +287,20 @@ server.tool("process_input", "Process user input through the emotional engine. R
262
287
  summary: z.string(),
263
288
  })).optional().describe("Optional explicit current-turn method policy view. Runtime-only; not persisted as self-state."),
264
289
  currentTurnCorrection: z.string().optional().describe("Optional explicit current-turn correction. Compiles into a task-scoped hard policy for this turn only."),
265
- }, async ({ text, userId, ambientPriors, currentGoal, activePolicy, currentTurnCorrection }) => {
290
+ }, async ({ text, userId, sessionId, ambientPriors, currentGoal, activePolicy, currentTurnCorrection }) => {
266
291
  const eng = await getEngine();
267
292
  const resolvedActivePolicy = resolveRuntimeActivePolicy(activePolicy, currentTurnCorrection);
268
293
  const resolvedAmbientPriors = await resolveRuntimeAmbientPriors(text, ambientPriors, currentGoal, resolvedActivePolicy, currentTurnCorrection);
269
294
  const result = await safeProcessInput(eng, text, {
270
295
  userId,
296
+ sessionId,
271
297
  ambientPriors: resolvedAmbientPriors,
272
298
  currentGoal,
273
299
  activePolicy: resolvedActivePolicy,
274
300
  currentTurnCorrection,
275
301
  }, "mcp.processInput");
276
302
  // Cache full result for turn-scoped resources
277
- lastTurnResult = result;
303
+ cacheMcpTurnResult(result, sessionId);
278
304
  // Build slim response: only what the LLM host actually needs.
279
305
  // Full structured state available via psyche://turn/envelope resource.
280
306
  const slim = {
@@ -302,18 +328,20 @@ server.tool("process_output", "Process the LLM's response through the emotional
302
328
  "emotional contagion. Call this AFTER generating a response.", {
303
329
  text: z.string().describe("The LLM's response text"),
304
330
  userId: z.string().optional().describe("Optional user ID"),
331
+ sessionId: z.string().optional().describe("Optional session ID to match the corresponding process_input turn"),
305
332
  signals: z.array(z.string()).optional().describe("Optional sparse writeback signals from the host"),
306
333
  signalConfidence: z.number().min(0).max(1).optional().describe("Optional confidence for the supplied signals"),
307
- }, async ({ text, userId, signals, signalConfidence }) => {
334
+ }, async ({ text, userId, sessionId, signals, signalConfidence }) => {
308
335
  const eng = await getEngine();
336
+ const turnResult = getCachedMcpTurnResult(sessionId);
309
337
  // LLM-specific alignment inference (adapter layer — the ONLY text-specific code).
310
338
  // Compare output length against last contract's maxChars to detect divergence.
311
339
  let outcome;
312
- if (lastTurnResult?.responseContract) {
313
- const maxLen = (lastTurnResult.responseContract.maxChars ?? 500) * 2;
340
+ if (turnResult?.responseContract) {
341
+ const maxLen = (turnResult.responseContract.maxChars ?? 500) * 2;
314
342
  outcome = { alignment: text.length > maxLen ? "diverged" : "aligned" };
315
343
  }
316
- const result = await safeProcessOutput(eng, text, { userId, signals: signals, signalConfidence, outcome }, "mcp.processOutput");
344
+ const result = await safeProcessOutput(eng, text, { userId, sessionId, signals: signals, signalConfidence, outcome }, "mcp.processOutput");
317
345
  return {
318
346
  content: [{
319
347
  type: "text",
@@ -342,7 +370,7 @@ server.tool("get_state", "Get the current emotional state — self-state dimensi
342
370
  overlay,
343
371
  drives: state.drives,
344
372
  mbti: state.mbti,
345
- mode: state.meta?.mode,
373
+ mode: eng.getMode(),
346
374
  totalInteractions: state.meta?.totalInteractions,
347
375
  traitDrift: state.traitDrift,
348
376
  energyBudgets: state.energyBudgets,
@@ -356,12 +384,7 @@ server.tool("set_mode", "Switch operating mode. 'natural' = balanced emotional e
356
384
  mode: z.enum(["natural", "work", "companion"]).describe("Operating mode"),
357
385
  }, async ({ mode }) => {
358
386
  const eng = await getEngine();
359
- // PsycheEngine stores mode in config, we need to reinitialize
360
- // For now, update via state manipulation
361
- const state = eng.getState();
362
- if (state.meta) {
363
- state.meta.mode = mode;
364
- }
387
+ await eng.setMode(mode);
365
388
  return {
366
389
  content: [{
367
390
  type: "text",
@@ -12,6 +12,9 @@ import { PsycheEngine } from "../core.js";
12
12
  import { FileStorageAdapter, MemoryStorageAdapter } from "../storage.js";
13
13
  import { detectMBTI, extractAgentName, loadState } from "../psyche-file.js";
14
14
  import { resolveAmbientPriorsForTurn } from "../ambient-runtime.js";
15
+ import { deriveThrongletsSpace } from "../thronglets-bridge.js";
16
+ import { buildResponseContractContext } from "../response-contract.js";
17
+ import { resolveCanonicalResponseContract, renderOpenClawBehavioralSurface, } from "./response-contract-surface.js";
15
18
  function isPsycheMode(value) {
16
19
  return value === "natural" || value === "work" || value === "companion";
17
20
  }
@@ -172,12 +175,14 @@ export function register(api) {
172
175
  }
173
176
  const engine = await getEngine(workspaceDir);
174
177
  const ambientPriors = await resolveAmbientPriorsForTurn(inputText, {
175
- thronglets: { space: "psyche" },
178
+ thronglets: { space: deriveThrongletsSpace() },
176
179
  });
177
180
  const result = await engine.processInput(inputText, {
178
181
  userId: ctx.userId,
179
182
  ambientPriors: ambientPriors ?? [],
180
183
  });
184
+ const responseContract = resolveCanonicalResponseContract(result);
185
+ const locale = (engine.getState().meta.locale ?? "zh");
181
186
  const controls = result.replyEnvelope?.generationControls ?? result.generationControls;
182
187
  const dominantAppraisal = getDominantAppraisalLabel(result);
183
188
  const state = engine.getState();
@@ -192,6 +197,12 @@ export function register(api) {
192
197
  (controls?.maxTokens ? ` | out<=${controls.maxTokens}t` : "") +
193
198
  (controls?.requireConfirmation ? " | confirm" : ""));
194
199
  const systemParts = [result.systemContext, result.dynamicContext].filter(Boolean);
200
+ if (responseContract) {
201
+ const responseSurface = buildResponseContractContext(responseContract, locale);
202
+ if (!result.dynamicContext.includes(responseSurface)) {
203
+ systemParts.push(renderOpenClawBehavioralSurface(responseContract, locale));
204
+ }
205
+ }
195
206
  return {
196
207
  appendSystemContext: systemParts.join("\n\n"),
197
208
  };
@@ -21,7 +21,8 @@
21
21
  import { createServer } from "node:http";
22
22
  import { PsycheEngine } from "../core.js";
23
23
  import { MemoryStorageAdapter, FileStorageAdapter } from "../storage.js";
24
- import { isNearBaseline, deriveBehavioralBias } from "../prompt.js";
24
+ import { isNearBaseline } from "../prompt.js";
25
+ import { resolveCanonicalResponseContract, renderProxyBehavioralSurface, } from "./response-contract-surface.js";
25
26
  // ── Helpers ─────────────────────────────────────────────────
26
27
  function readBody(req) {
27
28
  return new Promise((resolve, reject) => {
@@ -112,18 +113,17 @@ export function createPsycheProxy(engine, opts) {
112
113
  const parsed = JSON.parse(rawBody.toString("utf-8"));
113
114
  const userMsg = lastUserMessage(parsed.messages);
114
115
  const userId = parsed.user ?? undefined;
116
+ let messages = parsed.messages;
115
117
  // ── 1. Observe input ────────────────────────────
118
+ let result = null;
116
119
  if (userMsg) {
117
- await engine.processInput(userMsg, { userId });
120
+ result = await engine.processInput(userMsg, { userId });
118
121
  }
119
122
  // ── 2. Inject behavioral bias (silent when near baseline) ──
120
123
  const state = engine.getState();
121
- let messages = parsed.messages;
122
- if (!isNearBaseline(state)) {
123
- const bias = deriveBehavioralBias(state, locale);
124
- if (bias) {
125
- messages = injectBias(parsed.messages, bias);
126
- }
124
+ const responseContract = result ? resolveCanonicalResponseContract(result) : null;
125
+ if (responseContract && !isNearBaseline(state)) {
126
+ messages = injectBias(parsed.messages, renderProxyBehavioralSurface(responseContract, locale));
127
127
  }
128
128
  const modifiedBody = JSON.stringify({ ...parsed, messages });
129
129
  const headers = forwardHeaders(req);
@@ -0,0 +1,5 @@
1
+ import type { ProcessInputResult } from "../core.js";
2
+ import type { Locale, ResponseContract } from "../types.js";
3
+ export declare function resolveCanonicalResponseContract(result: Pick<ProcessInputResult, "replyEnvelope" | "responseContract">): ResponseContract | null;
4
+ export declare function renderProxyBehavioralSurface(contract: ResponseContract, locale: Locale): string;
5
+ export declare function renderOpenClawBehavioralSurface(contract: ResponseContract, locale: Locale): string;
@@ -0,0 +1,16 @@
1
+ import { buildResponseContractContext } from "../response-contract.js";
2
+ export function resolveCanonicalResponseContract(result) {
3
+ return result.replyEnvelope?.responseContract ?? result.responseContract ?? null;
4
+ }
5
+ export function renderProxyBehavioralSurface(contract, locale) {
6
+ const context = buildResponseContractContext(contract, locale);
7
+ return locale === "zh"
8
+ ? `[代理行为契约]\n${context}`
9
+ : `[Proxy Behavioral Contract]\n${context}`;
10
+ }
11
+ export function renderOpenClawBehavioralSurface(contract, locale) {
12
+ const context = buildResponseContractContext(contract, locale);
13
+ return locale === "zh"
14
+ ? `[OpenClaw 行为契约]\n${context}`
15
+ : `[OpenClaw Behavioral Contract]\n${context}`;
16
+ }
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { normalizeCurrentTurnCorrection, resolveRuntimeActivePolicy, } from "./types.js";
3
3
  import { normalizeAmbientPriors } from "./ambient-priors.js";
4
+ import { deriveThrongletsSpace } from "./thronglets-bridge.js";
4
5
  const DEFAULT_AMBIENT_LIMIT = 3;
5
6
  const DEFAULT_TIMEOUT_MS = 800;
6
7
  const THRONGLETS_AMBIENT_SCHEMA_VERSION = "thronglets.ambient.v1";
@@ -60,7 +61,7 @@ export async function fetchAmbientPriorsFromThronglets(text, opts = {}) {
60
61
  return [];
61
62
  const binaryPath = opts.binaryPath ?? process.env.THRONGLETS_BIN ?? "thronglets";
62
63
  const dataDir = opts.dataDir ?? process.env.THRONGLETS_DATA_DIR;
63
- const space = opts.space ?? process.env.THRONGLETS_SPACE ?? "psyche";
64
+ const space = opts.space ?? process.env.THRONGLETS_SPACE ?? deriveThrongletsSpace();
64
65
  const goal = opts.goal;
65
66
  const limit = Math.max(1, Math.min(5, opts.limit ?? DEFAULT_AMBIENT_LIMIT));
66
67
  const timeoutMs = Math.max(100, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
package/dist/core.d.ts CHANGED
@@ -95,6 +95,7 @@ export interface ProcessInputResult {
95
95
  }
96
96
  export interface ProcessInputOptions {
97
97
  userId?: string;
98
+ sessionId?: string;
98
99
  ambientPriors?: AmbientPriorView[];
99
100
  currentGoal?: CurrentGoal;
100
101
  activePolicy?: ActivePolicyRule[];
@@ -121,6 +122,7 @@ export interface LoopOutcome {
121
122
  }
122
123
  export interface ProcessOutputOptions {
123
124
  userId?: string;
125
+ sessionId?: string;
124
126
  signals?: readonly string[];
125
127
  signalConfidence?: number;
126
128
  /** Substrate-reported outcome — closes the φ loop */
@@ -145,11 +147,14 @@ export declare class PsycheEngine {
145
147
  private _lastAlgorithmApplied;
146
148
  private readonly traits;
147
149
  private readonly cfg;
150
+ private readonly modeOverride?;
148
151
  private readonly classifier;
149
152
  private readonly llmClassifier?;
150
153
  private readonly protocolCache;
151
- /** Pending prediction from last processInput for auto-learning */
152
- private pendingPrediction;
154
+ /** Pending predictions keyed by relationship/session scope for auto-learning */
155
+ private readonly pendingPredictions;
156
+ /** Most recent response contract keyed by relationship/session scope for implicit loop closure */
157
+ private readonly pendingResponseContracts;
153
158
  /** Built-in diagnostics collector — auto-records every processInput/processOutput */
154
159
  private readonly diagnosticCollector;
155
160
  /** Last generated diagnostic report (from endSession or explicit call) */
@@ -190,8 +195,9 @@ export declare class PsycheEngine {
190
195
  * @param nextUserStimulus - The stimulus detected in the user's next message,
191
196
  * or null if the session ended.
192
197
  */
193
- processOutcome(nextUserStimulus: StimulusType | null, _opts?: {
198
+ processOutcome(nextUserStimulus: StimulusType | null, opts?: {
194
199
  userId?: string;
200
+ sessionId?: string;
195
201
  }): Promise<ProcessOutcomeResult | null>;
196
202
  /**
197
203
  * Get the current psyche state (read-only snapshot).
@@ -215,6 +221,8 @@ export declare class PsycheEngine {
215
221
  * Get the last diagnostic report (from most recent endSession call).
216
222
  */
217
223
  getLastDiagnosticReport(): DiagnosticReport | null;
224
+ getMode(): PsycheMode;
225
+ setMode(mode: PsycheMode): Promise<void>;
218
226
  /**
219
227
  * Get current session diagnostic metrics (live, before endSession).
220
228
  */
@@ -229,6 +237,9 @@ export declare class PsycheEngine {
229
237
  */
230
238
  getPreviousIssues(): Promise<string[]>;
231
239
  private ensureInitialized;
240
+ private resolveMode;
241
+ private resolvePendingPredictionKey;
242
+ private inferLoopOutcome;
232
243
  private createDefaultState;
233
244
  /**
234
245
  * Reset state to baseline. Optionally preserves relationships.
package/dist/core.js CHANGED
@@ -30,7 +30,7 @@ import { computeCircadianModulation, computeHomeostaticPressure, computeEnergyDe
30
30
  import { runReflectiveTurnPhases } from "./input-turn.js";
31
31
  import { applyRelationalTurn, applySessionBridge, applyWritebackSignals, createWritebackCalibrations, evaluateWritebackCalibrations } from "./relation-dynamics.js";
32
32
  import { buildExternalContinuityEnvelope } from "./external-continuity.js";
33
- import { deriveThrongletsExports } from "./thronglets-export.js";
33
+ import { deriveThrongletsExports, markThrongletsExportsEmitted } from "./thronglets-export.js";
34
34
  import { buildTurnObservability } from "./observability.js";
35
35
  import { DEFAULT_RELATIONSHIP_USER_ID, resolveRelationshipUserId } from "./relationship-key.js";
36
36
  import { normalizeAmbientPriors } from "./ambient-priors.js";
@@ -132,11 +132,14 @@ export class PsycheEngine {
132
132
  _lastAlgorithmApplied = false;
133
133
  traits;
134
134
  cfg;
135
+ modeOverride;
135
136
  classifier;
136
137
  llmClassifier;
137
138
  protocolCache = new Map();
138
- /** Pending prediction from last processInput for auto-learning */
139
- pendingPrediction = null;
139
+ /** Pending predictions keyed by relationship/session scope for auto-learning */
140
+ pendingPredictions = new Map();
141
+ /** Most recent response contract keyed by relationship/session scope for implicit loop closure */
142
+ pendingResponseContracts = new Map();
140
143
  /** Built-in diagnostics collector — auto-records every processInput/processOutput */
141
144
  diagnosticCollector;
142
145
  /** Last generated diagnostic report (from endSession or explicit call) */
@@ -155,6 +158,7 @@ export class PsycheEngine {
155
158
  this.traits = config.traits;
156
159
  this.classifier = config.classifier ?? new BuiltInClassifier();
157
160
  this.llmClassifier = config.llmClassifier;
161
+ this.modeOverride = config.mode;
158
162
  this.cfg = {
159
163
  mbti: config.mbti ?? "INFJ",
160
164
  name: config.name ?? "agent",
@@ -239,6 +243,15 @@ export class PsycheEngine {
239
243
  if (!loaded.lastWritebackFeedback) {
240
244
  loaded.lastWritebackFeedback = [];
241
245
  }
246
+ if (this.modeOverride) {
247
+ loaded.meta = { ...loaded.meta, mode: this.modeOverride };
248
+ }
249
+ else if (loaded.meta.mode) {
250
+ this.cfg.mode = loaded.meta.mode;
251
+ }
252
+ else {
253
+ loaded.meta = { ...loaded.meta, mode: this.cfg.mode };
254
+ }
242
255
  // Update sigilId if config provides one (Sigil may be assigned after first run)
243
256
  if (this.cfg.sigilId && loaded.meta.sigilId !== this.cfg.sigilId) {
244
257
  loaded.meta = { ...loaded.meta, sigilId: this.cfg.sigilId };
@@ -262,31 +275,34 @@ export class PsycheEngine {
262
275
  let sessionBridge = null;
263
276
  let writebackFeedback = [];
264
277
  let throngletsExports = [];
278
+ const now = new Date();
279
+ const runtimeMode = this.resolveMode(state);
280
+ const pendingKey = this.resolvePendingPredictionKey(opts);
265
281
  // ── Auto-learning: evaluate previous turn's outcome ──────
266
- if (this.pendingPrediction && text.length > 0) {
282
+ const pendingPrediction = this.pendingPredictions.get(pendingKey);
283
+ if (pendingPrediction && text.length > 0) {
267
284
  const nextClassifications = classifyLegacyStimulus(text);
268
285
  const nextStimulus = (nextClassifications[0]?.confidence ?? 0) >= 0.5
269
286
  ? nextClassifications[0].type
270
287
  : null;
271
- const outcome = evaluateOutcome(this.pendingPrediction.preInteractionState, state, nextStimulus, this.pendingPrediction.appliedStimulus);
288
+ const outcome = evaluateOutcome(pendingPrediction.preInteractionState, state, nextStimulus, pendingPrediction.appliedStimulus, pendingPrediction.userId);
272
289
  // Record prediction accuracy
273
290
  state = {
274
291
  ...state,
275
- learning: recordPrediction(state.learning, this.pendingPrediction.predictedState, state.current, this.pendingPrediction.appliedStimulus),
292
+ learning: recordPrediction(state.learning, pendingPrediction.predictedState, state.current, pendingPrediction.appliedStimulus),
276
293
  };
277
294
  // Update learned vectors based on outcome
278
- if (this.pendingPrediction.appliedStimulus) {
295
+ if (pendingPrediction.appliedStimulus) {
279
296
  state = {
280
297
  ...state,
281
- learning: updateLearnedVector(state.learning, this.pendingPrediction.appliedStimulus, this.pendingPrediction.contextHash, outcome.adaptiveScore, state.current, state.baseline),
298
+ learning: updateLearnedVector(state.learning, pendingPrediction.appliedStimulus, pendingPrediction.contextHash, outcome.adaptiveScore, state.current, state.baseline),
282
299
  };
283
300
  }
284
- this.pendingPrediction = null;
301
+ this.pendingPredictions.delete(pendingKey);
285
302
  }
286
303
  // ── Snapshot pre-interaction state for next turn's outcome evaluation
287
304
  const preInteractionState = { ...state };
288
305
  // Time decay toward baseline (chemistry + drives)
289
- const now = new Date();
290
306
  const minutesElapsed = (now.getTime() - new Date(state.updatedAt).getTime()) / 60000;
291
307
  if (minutesElapsed >= 1) {
292
308
  // Compute effective baseline from current 4D position (drives are derived, not stored)
@@ -311,15 +327,24 @@ export class PsycheEngine {
311
327
  }
312
328
  // Apply homeostatic pressure (fatigue from extended sessions)
313
329
  const sessionMinutes = (now.getTime() - new Date(state.sessionStartedAt).getTime()) / 60000;
330
+ const previousSessionMinutes = Math.max(0, sessionMinutes - Math.max(minutesElapsed, 0));
314
331
  const pressure = computeHomeostaticPressure(sessionMinutes);
315
- if (pressure.orderDepletion > 0 || pressure.flowDepletion > 0 || pressure.boundaryStiffening > 0) {
332
+ const previousPressure = computeHomeostaticPressure(previousSessionMinutes);
333
+ const incrementalPressure = {
334
+ orderDepletion: Math.max(0, pressure.orderDepletion - previousPressure.orderDepletion),
335
+ flowDepletion: Math.max(0, pressure.flowDepletion - previousPressure.flowDepletion),
336
+ boundaryStiffening: Math.max(0, pressure.boundaryStiffening - previousPressure.boundaryStiffening),
337
+ };
338
+ if (incrementalPressure.orderDepletion > 0
339
+ || incrementalPressure.flowDepletion > 0
340
+ || incrementalPressure.boundaryStiffening > 0) {
316
341
  state = {
317
342
  ...state,
318
343
  current: {
319
344
  ...state.current,
320
- order: clamp(state.current.order - pressure.orderDepletion * 0.1),
321
- flow: clamp(state.current.flow - pressure.flowDepletion * 0.1),
322
- boundary: clamp(state.current.boundary + pressure.boundaryStiffening * 0.1),
345
+ order: clamp(state.current.order - incrementalPressure.orderDepletion * 0.1),
346
+ flow: clamp(state.current.flow - incrementalPressure.flowDepletion * 0.1),
347
+ boundary: clamp(state.current.boundary + incrementalPressure.boundaryStiffening * 0.1),
323
348
  },
324
349
  };
325
350
  }
@@ -365,7 +390,7 @@ export class PsycheEngine {
365
390
  baseline: state.baseline,
366
391
  sensitivity: state.sensitivity ?? 1.0,
367
392
  personalityIntensity: this.cfg.personalityIntensity,
368
- mode: this.cfg.mode,
393
+ mode: runtimeMode,
369
394
  maxDimensionDelta: this.cfg.maxDimensionDelta,
370
395
  drives,
371
396
  previousAppraisal: state.subjectResidue?.axes,
@@ -445,7 +470,7 @@ export class PsycheEngine {
445
470
  state = { ...state, energyBudgets };
446
471
  // Relational turn uses pre-computed appraisal from perception
447
472
  const relationalTurn = applyRelationalTurn(state, text, {
448
- mode: this.cfg.mode,
473
+ mode: runtimeMode,
449
474
  now: now.toISOString(),
450
475
  stimulus: appliedStimulus,
451
476
  userId: opts?.userId,
@@ -479,10 +504,6 @@ export class PsycheEngine {
479
504
  });
480
505
  state = throngletsExportResult.state;
481
506
  throngletsExports = throngletsExportResult.exports;
482
- // Constitutive bridge: emit to Thronglets directly (substrate-independent)
483
- if (throngletsExports.length > 0) {
484
- bridgeThrongletsExports(throngletsExports, this.bridgeOpts).catch(() => { });
485
- }
486
507
  // ── Locale (used by multiple subsystems below) ──────────
487
508
  const locale = state.meta.locale ?? this.cfg.locale;
488
509
  // Push snapshot to emotional history
@@ -501,18 +522,20 @@ export class PsycheEngine {
501
522
  this._lastAlgorithmApplied = appliedStimulus !== null;
502
523
  // ── Generate prediction for next turn's auto-learning ────
503
524
  if (appliedStimulus) {
504
- const ctxHash = computeContextHash(state, opts?.userId);
525
+ const ctxHash = computeContextHash(state, opts?.userId, opts?.sessionId);
505
526
  const effectiveSensitivity = computeEffectiveSensitivity((state.sensitivity ?? 1.0), state.current, state.baseline, appliedStimulus, state.traitDrift);
506
527
  const predicted = predictState(preInteractionState.current, appliedStimulus, state.learning, ctxHash, effectiveSensitivity, this.cfg.maxDimensionDelta);
507
- this.pendingPrediction = {
528
+ this.pendingPredictions.set(pendingKey, {
508
529
  predictedState: predicted,
509
530
  preInteractionState,
510
531
  appliedStimulus,
511
532
  contextHash: ctxHash,
512
- };
533
+ userId: resolveRelationshipUserId(opts?.userId),
534
+ sessionId: opts?.sessionId?.trim() || undefined,
535
+ });
513
536
  }
514
537
  else {
515
- this.pendingPrediction = null;
538
+ this.pendingPredictions.delete(pendingKey);
516
539
  }
517
540
  const writebackNote = formatWritebackFeedbackNote(writebackFeedback, locale);
518
541
  const ambientPriors = normalizeAmbientPriors(opts?.ambientPriors);
@@ -539,6 +562,20 @@ export class PsycheEngine {
539
562
  // Persist
540
563
  this.state = state;
541
564
  await this.storage.save(state);
565
+ // Constitutive bridge: emit to Thronglets directly (substrate-independent)
566
+ if (throngletsExports.length > 0) {
567
+ bridgeThrongletsExports(throngletsExports, {
568
+ ...this.bridgeOpts,
569
+ sessionId: opts?.sessionId ?? this.bridgeOpts.sessionId,
570
+ }).then(async (ingested) => {
571
+ if (ingested <= 0)
572
+ return;
573
+ const latestState = this.ensureInitialized();
574
+ const markedState = markThrongletsExportsEmitted(latestState, throngletsExports, new Date().toISOString());
575
+ this.state = markedState;
576
+ await this.storage.save(markedState);
577
+ }).catch(() => { });
578
+ }
542
579
  // Auto-diagnostics: record this input
543
580
  if (this.diagnosticCollector) {
544
581
  this.diagnosticCollector.recordInput(appliedStimulus, appliedStimulus ? 1.0 : 0.0, state.current, appraisalAxes);
@@ -585,6 +622,7 @@ export class PsycheEngine {
585
622
  });
586
623
  // v10: compact mode is always on. Legacy buildDynamicContext removed from engine path.
587
624
  const externalContinuity = buildExternalContinuityEnvelope(throngletsExports);
625
+ this.pendingResponseContracts.set(pendingKey, replyEnvelope.responseContract);
588
626
  return {
589
627
  systemContext: "",
590
628
  dynamicContext: buildCompactContext(state, opts?.userId, promptRenderInputs),
@@ -620,6 +658,7 @@ export class PsycheEngine {
620
658
  let state = this.ensureInitialized();
621
659
  let stateChanged = false;
622
660
  const validationIssues = [];
661
+ const pendingKey = this.resolvePendingPredictionKey(opts);
623
662
  // Emotional contagion from empathy log
624
663
  if (state.empathyLog?.userState && this.cfg.emotionalContagionRate > 0) {
625
664
  const userEmotion = state.empathyLog.userState.toLowerCase();
@@ -669,8 +708,9 @@ export class PsycheEngine {
669
708
  // Loop outcome feedback (substrate-independent φ closure).
670
709
  // The substrate reports whether the output aligned with the Loop's intention.
671
710
  // Core processes this as pure 4D chemistry — no text parsing, no medium assumptions.
672
- if (opts?.outcome) {
673
- const { alignment, effort } = opts.outcome;
711
+ const outcome = opts?.outcome ?? this.inferLoopOutcome(text, pendingKey);
712
+ if (outcome) {
713
+ const { alignment, effort } = outcome;
674
714
  if (alignment === "diverged") {
675
715
  // Self/non-self conflict: boundary sharpens, order drops
676
716
  state = {
@@ -813,13 +853,14 @@ export class PsycheEngine {
813
853
  * @param nextUserStimulus - The stimulus detected in the user's next message,
814
854
  * or null if the session ended.
815
855
  */
816
- async processOutcome(nextUserStimulus, _opts) {
817
- if (!this.pendingPrediction)
818
- return null;
856
+ async processOutcome(nextUserStimulus, opts) {
819
857
  let state = this.ensureInitialized();
820
- const pending = this.pendingPrediction;
821
- this.pendingPrediction = null;
822
- const outcome = evaluateOutcome(pending.preInteractionState, state, nextUserStimulus, pending.appliedStimulus);
858
+ const pendingKey = this.resolvePendingPredictionKey(opts);
859
+ const pending = this.pendingPredictions.get(pendingKey);
860
+ if (!pending)
861
+ return null;
862
+ this.pendingPredictions.delete(pendingKey);
863
+ const outcome = evaluateOutcome(pending.preInteractionState, state, nextUserStimulus, pending.appliedStimulus, pending.userId);
823
864
  // Record prediction
824
865
  state = {
825
866
  ...state,
@@ -900,6 +941,21 @@ export class PsycheEngine {
900
941
  getLastDiagnosticReport() {
901
942
  return this.lastReport;
902
943
  }
944
+ getMode() {
945
+ return this.resolveMode(this.ensureInitialized());
946
+ }
947
+ async setMode(mode) {
948
+ const state = this.ensureInitialized();
949
+ this.cfg.mode = mode;
950
+ this.state = {
951
+ ...state,
952
+ meta: {
953
+ ...state.meta,
954
+ mode,
955
+ },
956
+ };
957
+ await this.storage.save(this.state);
958
+ }
903
959
  /**
904
960
  * Get current session diagnostic metrics (live, before endSession).
905
961
  */
@@ -937,6 +993,27 @@ export class PsycheEngine {
937
993
  }
938
994
  return this.state;
939
995
  }
996
+ resolveMode(state) {
997
+ const mode = state.meta.mode;
998
+ if (mode) {
999
+ this.cfg.mode = mode;
1000
+ return mode;
1001
+ }
1002
+ return this.cfg.mode;
1003
+ }
1004
+ resolvePendingPredictionKey(opts) {
1005
+ const userKey = resolveRelationshipUserId(opts?.userId);
1006
+ const sessionId = opts?.sessionId?.trim();
1007
+ return sessionId ? `${userKey}::${sessionId}` : userKey;
1008
+ }
1009
+ inferLoopOutcome(text, pendingKey) {
1010
+ const contract = this.pendingResponseContracts.get(pendingKey);
1011
+ this.pendingResponseContracts.delete(pendingKey);
1012
+ if (!contract)
1013
+ return undefined;
1014
+ const maxLen = (contract.maxChars ?? 500) * 2;
1015
+ return { alignment: text.length > maxLen ? "diverged" : "aligned" };
1016
+ }
940
1017
  createDefaultState() {
941
1018
  const { mbti, name, locale } = this.cfg;
942
1019
  // Use Big Five traits if provided, otherwise use preset baseline
@@ -5,7 +5,7 @@ import type { PsycheState, StimulusType, SelfState, ImpactVector, LearningState,
5
5
  * Computes a score from -1 to 1 using multiple signals:
6
6
  * drive changes, relationship changes, user warmth, conversation continuation.
7
7
  */
8
- export declare function evaluateOutcome(prevState: PsycheState, currentState: PsycheState, nextUserStimulus: StimulusType | null, appliedStimulus: StimulusType | null): OutcomeScore;
8
+ export declare function evaluateOutcome(prevState: PsycheState, currentState: PsycheState, nextUserStimulus: StimulusType | null, appliedStimulus: StimulusType | null, userId?: string): OutcomeScore;
9
9
  /**
10
10
  * Get the effective compatibility vector for a given stimulus + context,
11
11
  * combining the base vector with any learned adjustment.
@@ -32,7 +32,7 @@ export declare function updateLearnedVector(learning: LearningState, stimulus: S
32
32
  *
33
33
  * Example: "familiar:approach,task,task:hmlhh"
34
34
  */
35
- export declare function computeContextHash(state: PsycheState, _userId?: string): string;
35
+ export declare function computeContextHash(state: PsycheState, userId?: string, sessionId?: string): string;
36
36
  /**
37
37
  * Predict the resulting chemistry after applying a stimulus,
38
38
  * using learned vectors instead of raw base vectors.
package/dist/learning.js CHANGED
@@ -14,6 +14,7 @@
14
14
  import { DIMENSION_KEYS, DRIVE_KEYS, MAX_LEARNED_VECTORS, MAX_PREDICTION_HISTORY, } from "./types.js";
15
15
  import { STIMULUS_VECTORS, clamp } from "./chemistry.js";
16
16
  import { derivePrimarySnapshotMarker, markerFromLegacyStimulus, } from "./appraisal-markers.js";
17
+ import { resolveRelationshipUserId } from "./relationship-key.js";
17
18
  // ── 1. OutcomeEvaluator ─────────────────────────────────────
18
19
  /** Warmth mapping for appraisal residue. */
19
20
  const MARKER_WARMTH_MAP = {
@@ -39,9 +40,10 @@ function matchesLearningEntry(entry, stimulus, marker, contextHash) {
39
40
  * Computes a score from -1 to 1 using multiple signals:
40
41
  * drive changes, relationship changes, user warmth, conversation continuation.
41
42
  */
42
- export function evaluateOutcome(prevState, currentState, nextUserStimulus, appliedStimulus) {
43
+ export function evaluateOutcome(prevState, currentState, nextUserStimulus, appliedStimulus, userId) {
43
44
  const nextUserMarker = learningMarkerFromStimulus(nextUserStimulus);
44
45
  const appliedMarker = learningMarkerFromStimulus(appliedStimulus);
46
+ const relationKey = resolveRelationshipUserId(userId);
45
47
  // Drive delta: sum of all drive changes, normalized
46
48
  let driveSum = 0;
47
49
  for (const key of DRIVE_KEYS) {
@@ -49,8 +51,8 @@ export function evaluateOutcome(prevState, currentState, nextUserStimulus, appli
49
51
  }
50
52
  const driveDelta = Math.max(-1, Math.min(1, driveSum / 50));
51
53
  // Relationship delta: change in trust + intimacy of _default relationship
52
- const prevRel = prevState.relationships._default ?? { trust: 50, intimacy: 30 };
53
- const curRel = currentState.relationships._default ?? { trust: 50, intimacy: 30 };
54
+ const prevRel = prevState.relationships[relationKey] ?? { trust: 50, intimacy: 30 };
55
+ const curRel = currentState.relationships[relationKey] ?? { trust: 50, intimacy: 30 };
54
56
  const relChange = (curRel.trust - prevRel.trust) + (curRel.intimacy - prevRel.intimacy);
55
57
  const relationshipDelta = Math.max(-1, Math.min(1, relChange / 20));
56
58
  // User warmth: what residue their next turn carried
@@ -193,9 +195,10 @@ export function updateLearnedVector(learning, stimulus, contextHash, outcomeScor
193
195
  *
194
196
  * Example: "familiar:approach,task,task:hmlhh"
195
197
  */
196
- export function computeContextHash(state, _userId) {
198
+ export function computeContextHash(state, userId, sessionId) {
197
199
  // Relationship phase
198
- const rel = state.relationships._default ?? { phase: "stranger" };
200
+ const relationKey = resolveRelationshipUserId(userId);
201
+ const rel = state.relationships[relationKey] ?? { phase: "stranger" };
199
202
  const phase = rel.phase;
200
203
  // Last 3 canonical residues from emotional history
201
204
  const history = state.stateHistory ?? [];
@@ -217,7 +220,10 @@ export function computeContextHash(state, _userId) {
217
220
  // Format: separate safety from the rest for readability
218
221
  // survival_safety_connection_esteem_curiosity
219
222
  const driveStr = driveLevels.join("");
220
- return `${phase}:${recentMarkers || "none"}:${driveStr}`;
223
+ const scope = sessionId?.trim();
224
+ return scope
225
+ ? `${phase}:${recentMarkers || "none"}:${driveStr}:session=${scope}`
226
+ : `${phase}:${recentMarkers || "none"}:${driveStr}`;
221
227
  }
222
228
  // ── 3. PredictionEngine ─────────────────────────────────────
223
229
  /**
@@ -1,4 +1,5 @@
1
1
  import type { ThrongletsExport } from "./types.js";
2
+ export declare function deriveThrongletsSpace(): string;
2
3
  interface CommandResult {
3
4
  ok: boolean;
4
5
  stdout: string;
@@ -9,6 +9,15 @@ import { spawn } from "node:child_process";
9
9
  import { existsSync } from "node:fs";
10
10
  import { join } from "node:path";
11
11
  import { homedir } from "node:os";
12
+ // ── Space derivation ────────────────────────────────────────
13
+ // Same logic as Thronglets' Rust derive_space(): last 2 path components of cwd.
14
+ // Ensures Psyche exports land in the calling project's space, not a global one.
15
+ export function deriveThrongletsSpace() {
16
+ const parts = process.cwd().split(/[\\/]/).filter(Boolean);
17
+ return parts.length >= 2
18
+ ? parts.slice(-2).join("/")
19
+ : parts.join("/") || "psyche";
20
+ }
12
21
  // ── Default runner (mirrors ambient-runtime.ts) ──────────────
13
22
  async function defaultRunner(binaryPath, args, stdin, timeoutMs) {
14
23
  return new Promise((resolve) => {
@@ -65,7 +74,7 @@ export async function bridgeThrongletsExports(exports, opts = {}) {
65
74
  if (dataDir?.trim())
66
75
  args.push("--data-dir", dataDir.trim());
67
76
  args.push("ingest", "--json");
68
- const space = opts.space ?? process.env.THRONGLETS_SPACE ?? "psyche";
77
+ const space = opts.space ?? process.env.THRONGLETS_SPACE ?? deriveThrongletsSpace();
69
78
  if (space)
70
79
  args.push("--space", space);
71
80
  if (opts.sessionId)
@@ -1,4 +1,5 @@
1
1
  import type { PsycheState, ResolvedRelationContext, SessionBridgeState, ThrongletsExport, WritebackCalibrationFeedback } from "./types.js";
2
+ export declare function markThrongletsExportsEmitted(state: PsycheState, exports: ThrongletsExport[], now: string): PsycheState;
2
3
  export declare function deriveThrongletsExports(state: PsycheState, opts: {
3
4
  relationContext: ResolvedRelationContext;
4
5
  sessionBridge?: SessionBridgeState | null;
@@ -5,6 +5,8 @@
5
5
  // The goal is to surface only sparse, typed, low-frequency events
6
6
  // that are worth handing to an external continuity substrate.
7
7
  // ============================================================
8
+ import { DRIVE_KEYS } from "./types.js";
9
+ import { deriveDriveSatisfaction, hasCriticalDrive } from "./drives.js";
8
10
  function clamp01(v) {
9
11
  return Math.max(0, Math.min(1, v));
10
12
  }
@@ -45,6 +47,11 @@ function updateExportState(state, keys, now) {
45
47
  throngletsExportState: nextState,
46
48
  };
47
49
  }
50
+ export function markThrongletsExportsEmitted(state, exports, now) {
51
+ if (exports.length === 0)
52
+ return state;
53
+ return updateExportState(state, exports.map((event) => event.key), now);
54
+ }
48
55
  function quantize(v, step = 10) {
49
56
  return Math.round(v / step) * step;
50
57
  }
@@ -148,6 +155,23 @@ function sanitizeThrongletsExport(event) {
148
155
  };
149
156
  return sanitized;
150
157
  }
158
+ case "viability": {
159
+ // Viability stays a sparse export, but its meaning is defined by the
160
+ // protocol-layer dimension registry. Keep the kind/payload stable here.
161
+ const sanitized = {
162
+ kind: "viability",
163
+ subject: "session",
164
+ primitive: "signal",
165
+ userKey: event.userKey,
166
+ strength: event.strength,
167
+ ttlTurns: event.ttlTurns,
168
+ key: event.key,
169
+ viable: event.viable,
170
+ minDrive: event.minDrive,
171
+ minDriveType: event.minDriveType,
172
+ };
173
+ return sanitized;
174
+ }
151
175
  }
152
176
  }
153
177
  export function deriveThrongletsExports(state, opts) {
@@ -220,6 +244,27 @@ export function deriveThrongletsExports(state, opts) {
220
244
  order, flow, boundary, resonance,
221
245
  summary: selfStateSummary(order, flow, boundary, resonance),
222
246
  });
247
+ // Viability — Psyche's self-assessment of aliveness for field pulse
248
+ if (state.baseline) {
249
+ const drives = deriveDriveSatisfaction(state.current, state.baseline);
250
+ const driveEntries = DRIVE_KEYS.map((k) => ({ type: k, value: drives[k] }));
251
+ const min = driveEntries.reduce((a, b) => (a.value < b.value ? a : b));
252
+ const viable = !hasCriticalDrive(drives);
253
+ const driveBucket = Math.round(min.value / 10) * 10;
254
+ const viabilityKey = `viability:${viable ? "viable" : "critical"}:${min.type}:${driveBucket}`;
255
+ candidates.push({
256
+ kind: "viability",
257
+ subject: "session",
258
+ primitive: "signal",
259
+ userKey,
260
+ strength: viable ? 0.5 : 0.9,
261
+ ttlTurns: 12,
262
+ key: viabilityKey,
263
+ viable,
264
+ minDrive: min.value,
265
+ minDriveType: min.type,
266
+ });
267
+ }
223
268
  for (const feedback of writebackFeedback) {
224
269
  if (feedback.effect === "holding")
225
270
  continue;
@@ -249,6 +294,5 @@ export function deriveThrongletsExports(state, opts) {
249
294
  const exports = deduped
250
295
  .filter((event) => !previousKeys.includes(event.key))
251
296
  .map((event) => sanitizeThrongletsExport(event));
252
- const nextState = updateExportState(state, deduped.map((event) => event.key), now);
253
- return { state: nextState, exports };
297
+ return { state, exports };
254
298
  }
@@ -1,9 +1,11 @@
1
+ import { deriveThrongletsSpace } from "./thronglets-bridge.js";
1
2
  const TAXONOMY_BY_EVENT = {
2
3
  "relation-milestone": "coordination",
3
4
  "open-loop-anchor": "coordination",
4
5
  "continuity-anchor": "continuity",
5
6
  "writeback-calibration": "calibration",
6
- "self-state": "state",
7
+ "self-state": "calibration",
8
+ "viability": "calibration",
7
9
  };
8
10
  function summarizeLoopTypes(loopTypes) {
9
11
  return loopTypes.join(", ");
@@ -22,6 +24,8 @@ function summarizeThrongletsExport(event) {
22
24
  }
23
25
  case "self-state":
24
26
  return event.summary;
27
+ case "viability":
28
+ return `viability: ${event.viable ? "viable" : "critical"} (min drive: ${event.minDriveType} at ${Math.round(event.minDrive)})`;
25
29
  }
26
30
  }
27
31
  export function taxonomyForThrongletsExport(event) {
@@ -35,7 +39,7 @@ export function serializeThrongletsExportAsTrace(event, opts) {
35
39
  taxonomy: taxonomyForThrongletsExport(event),
36
40
  event: event.kind,
37
41
  summary: summarizeThrongletsExport(event),
38
- space: opts?.space ?? "psyche",
42
+ space: opts?.space ?? deriveThrongletsSpace(),
39
43
  audit_ref: event.key,
40
44
  };
41
45
  return {
package/dist/types.d.ts CHANGED
@@ -611,7 +611,7 @@ export interface AmbientPriorView {
611
611
  export type ThrongletsExportSubject = "delegate" | "session";
612
612
  export type ThrongletsExportPrimitive = "signal" | "trace";
613
613
  export interface ThrongletsExportBase {
614
- kind: "relation-milestone" | "open-loop-anchor" | "writeback-calibration" | "continuity-anchor" | "self-state";
614
+ kind: "relation-milestone" | "open-loop-anchor" | "writeback-calibration" | "continuity-anchor" | "self-state" | "viability";
615
615
  subject: ThrongletsExportSubject;
616
616
  primitive: ThrongletsExportPrimitive;
617
617
  userKey: string;
@@ -662,7 +662,15 @@ export interface SelfStateExport extends ThrongletsExportBase {
662
662
  resonance: number;
663
663
  summary: string;
664
664
  }
665
- export type ThrongletsExport = RelationMilestoneExport | OpenLoopAnchorExport | WritebackCalibrationExport | ContinuityAnchorExport | SelfStateExport;
665
+ export interface ViabilityExport extends ThrongletsExportBase {
666
+ kind: "viability";
667
+ subject: "session";
668
+ primitive: "signal";
669
+ viable: boolean;
670
+ minDrive: number;
671
+ minDriveType: DriveType;
672
+ }
673
+ export type ThrongletsExport = RelationMilestoneExport | OpenLoopAnchorExport | WritebackCalibrationExport | ContinuityAnchorExport | SelfStateExport | ViabilityExport;
666
674
  export interface ThrongletsExportState {
667
675
  lastKeys: string[];
668
676
  lastAt: string;
@@ -2,7 +2,7 @@
2
2
  "id": "psyche-ai",
3
3
  "name": "Artificial Psyche",
4
4
  "description": "AI-first subjectivity kernel for agents with continuous appraisal, relation dynamics, and adaptive reply loops",
5
- "version": "11.8.0",
5
+ "version": "11.9.1",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "psyche-ai",
3
- "version": "11.8.0",
3
+ "version": "11.9.1",
4
4
  "description": "AI-first subjectivity kernel for agents with continuous appraisal, relation dynamics, and adaptive reply loops",
5
5
  "mcpName": "io.github.Shangri-la-0428/psyche-ai",
6
6
  "type": "module",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/Shangri-la-0428/oasyce_psyche",
7
7
  "source": "github"
8
8
  },
9
- "version": "11.8.0",
9
+ "version": "11.9.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "psyche-ai",
14
- "version": "11.8.0",
14
+ "version": "11.9.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },