@neurolift-technologies/sleepwalker-protocol 1.0.0 → 1.0.2
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/README.md +8 -0
- package/dist/continuity.d.ts +20 -0
- package/dist/continuity.js +37 -3
- package/dist/protocol.d.ts +16 -1
- package/dist/protocol.js +39 -9
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
> ## ⚠️ PROTOTYPE — NOT A SAFETY SYSTEM
|
|
2
|
+
>
|
|
3
|
+
> This is an **experimental** crisis-detection library with **stubbed/placeholder intervention layers**. It is **NOT medical advice, NOT a crisis service**, and performs **no real-time monitoring**. It **can miss real crisis signals** (known detection/recall gaps) — **do not rely on it as a safety net or as the sole safety mechanism**.
|
|
4
|
+
>
|
|
5
|
+
> **If you or someone else needs help now:** in the US, call or text **988** (Suicide & Crisis Lifeline) or chat [988lifeline.org](https://988lifeline.org); in an emergency call **911**. Outside the US: [findahelpline.com](https://findahelpline.com).
|
|
6
|
+
>
|
|
7
|
+
> Provided **AS-IS, without warranty**.
|
|
8
|
+
|
|
1
9
|
# Sleepwalker Protocol (SWP)
|
|
2
10
|
|
|
3
11
|
```yaml
|
package/dist/continuity.d.ts
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Temporal Continuity Management Module
|
|
3
|
+
*
|
|
4
|
+
* Maintains emotional state and boundary awareness across sessions.
|
|
3
5
|
*/
|
|
4
6
|
export declare class ContinuityManager {
|
|
5
7
|
private storagePath;
|
|
6
8
|
constructor(storagePath?: string);
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the storage file for a user — traversal-safe and collision-safe.
|
|
11
|
+
*
|
|
12
|
+
* `userId` becomes part of a filename, which raises two risks:
|
|
13
|
+
*
|
|
14
|
+
* - **Path traversal.** A raw value like `"../../etc/passwd"` must not read or
|
|
15
|
+
* write outside `storagePath`.
|
|
16
|
+
* - **Collisions.** Simply stripping disallowed characters would map distinct
|
|
17
|
+
* ids onto the same file (`"a/b"` and `"ab"`, or `"alice@example.com"` and
|
|
18
|
+
* `"aliceexample.com"`), letting one user's continuity overwrite or leak
|
|
19
|
+
* into another's.
|
|
20
|
+
*
|
|
21
|
+
* We therefore key the file on a SHA-256 of the *full* id — deterministic,
|
|
22
|
+
* collision-resistant, and filesystem-safe (hex only) — prefixed with a
|
|
23
|
+
* sanitized, human-readable slug purely for debuggability. Mirrors the Python
|
|
24
|
+
* `ContinuityManager._user_file`.
|
|
25
|
+
*/
|
|
26
|
+
private userFile;
|
|
7
27
|
saveSession(userId: string, sessionData: any): void;
|
|
8
28
|
getContext(userId: string): any;
|
|
9
29
|
private loadUserData;
|
package/dist/continuity.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Temporal Continuity Management Module
|
|
4
|
+
*
|
|
5
|
+
* Maintains emotional state and boundary awareness across sessions.
|
|
4
6
|
*/
|
|
5
7
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
8
|
if (k2 === undefined) k2 = k;
|
|
@@ -37,6 +39,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
37
39
|
})();
|
|
38
40
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
41
|
exports.ContinuityManager = void 0;
|
|
42
|
+
const crypto = __importStar(require("crypto"));
|
|
40
43
|
const fs = __importStar(require("fs"));
|
|
41
44
|
const path = __importStar(require("path"));
|
|
42
45
|
class ContinuityManager {
|
|
@@ -46,27 +49,58 @@ class ContinuityManager {
|
|
|
46
49
|
fs.mkdirSync(this.storagePath, { recursive: true });
|
|
47
50
|
}
|
|
48
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the storage file for a user — traversal-safe and collision-safe.
|
|
54
|
+
*
|
|
55
|
+
* `userId` becomes part of a filename, which raises two risks:
|
|
56
|
+
*
|
|
57
|
+
* - **Path traversal.** A raw value like `"../../etc/passwd"` must not read or
|
|
58
|
+
* write outside `storagePath`.
|
|
59
|
+
* - **Collisions.** Simply stripping disallowed characters would map distinct
|
|
60
|
+
* ids onto the same file (`"a/b"` and `"ab"`, or `"alice@example.com"` and
|
|
61
|
+
* `"aliceexample.com"`), letting one user's continuity overwrite or leak
|
|
62
|
+
* into another's.
|
|
63
|
+
*
|
|
64
|
+
* We therefore key the file on a SHA-256 of the *full* id — deterministic,
|
|
65
|
+
* collision-resistant, and filesystem-safe (hex only) — prefixed with a
|
|
66
|
+
* sanitized, human-readable slug purely for debuggability. Mirrors the Python
|
|
67
|
+
* `ContinuityManager._user_file`.
|
|
68
|
+
*/
|
|
69
|
+
userFile(userId) {
|
|
70
|
+
const raw = String(userId);
|
|
71
|
+
const slug = (raw.match(/[A-Za-z0-9_-]/g) || []).join('').slice(0, 32) || 'user';
|
|
72
|
+
// Full SHA-256 hex (the filename is the user-isolation boundary; the extra
|
|
73
|
+
// length is free and maximizes separation between users).
|
|
74
|
+
const digest = crypto.createHash('sha256').update(raw, 'utf-8').digest('hex');
|
|
75
|
+
return path.join(this.storagePath, `${slug}-${digest}.json`);
|
|
76
|
+
}
|
|
49
77
|
saveSession(userId, sessionData) {
|
|
50
|
-
const userFile =
|
|
78
|
+
const userFile = this.userFile(userId);
|
|
51
79
|
const existingData = this.loadUserData(userId);
|
|
52
80
|
sessionData.timestamp = new Date().toISOString();
|
|
53
81
|
existingData.lastSession = sessionData;
|
|
54
82
|
existingData.sessionCount = (existingData.sessionCount || 0) + 1;
|
|
83
|
+
// Preserve declared boundaries across sessions.
|
|
84
|
+
if (sessionData.declaredBoundaries !== undefined) {
|
|
85
|
+
existingData.declaredBoundaries = sessionData.declaredBoundaries;
|
|
86
|
+
}
|
|
55
87
|
fs.writeFileSync(userFile, JSON.stringify(existingData, null, 2));
|
|
56
88
|
}
|
|
57
89
|
getContext(userId) {
|
|
58
90
|
const userData = this.loadUserData(userId);
|
|
59
91
|
if (Object.keys(userData).length === 0) {
|
|
60
|
-
return { hasHistory: false, protectiveStateActive: false };
|
|
92
|
+
return { hasHistory: false, protectiveStateActive: false, declaredBoundaries: [] };
|
|
61
93
|
}
|
|
62
94
|
return {
|
|
63
95
|
hasHistory: true,
|
|
64
96
|
lastSessionState: userData.lastSession?.emotionalState || 'unknown',
|
|
65
97
|
protectiveStateActive: userData.lastSession?.protectiveStateActive || false,
|
|
98
|
+
declaredBoundaries: userData.declaredBoundaries || [],
|
|
99
|
+
sessionCount: userData.sessionCount || 0,
|
|
66
100
|
};
|
|
67
101
|
}
|
|
68
102
|
loadUserData(userId) {
|
|
69
|
-
const userFile =
|
|
103
|
+
const userFile = this.userFile(userId);
|
|
70
104
|
if (!fs.existsSync(userFile))
|
|
71
105
|
return {};
|
|
72
106
|
try {
|
package/dist/protocol.d.ts
CHANGED
|
@@ -7,16 +7,31 @@ export interface SWPOptions {
|
|
|
7
7
|
privacyMode?: string;
|
|
8
8
|
loggingEnabled?: boolean;
|
|
9
9
|
storagePath?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Stable identifier for the user this instance serves. Used as the continuity
|
|
12
|
+
* key when `assessInteraction` is called without an explicit `userId`. Falls
|
|
13
|
+
* back to a `userId` declared in the TOI, or to `'default_user'` so a single
|
|
14
|
+
* instance still maintains continuity across calls.
|
|
15
|
+
*/
|
|
16
|
+
userId?: string;
|
|
10
17
|
}
|
|
11
18
|
export declare class SleepwalkerProtocol {
|
|
12
19
|
private userToi;
|
|
20
|
+
private userId;
|
|
13
21
|
private stateDetector;
|
|
14
22
|
private consentManager;
|
|
15
23
|
private continuityManager;
|
|
16
24
|
private loggingEnabled;
|
|
17
25
|
constructor(options?: SWPOptions);
|
|
18
26
|
detectEmotionalState(userInput: string, sessionHistory?: any[]): EmotionalState;
|
|
19
|
-
assessInteraction(userInput: string, sessionHistory?: any[]): any;
|
|
27
|
+
assessInteraction(userInput: string, sessionHistory?: any[], userId?: string): any;
|
|
28
|
+
/**
|
|
29
|
+
* Preserves emotional boundaries across sessions. Mirrors Python
|
|
30
|
+
* `maintain_continuity`: the read (`assessInteraction`) and the write
|
|
31
|
+
* (`maintainContinuity`) stay separate so that assessment never implicitly
|
|
32
|
+
* writes to disk.
|
|
33
|
+
*/
|
|
34
|
+
maintainContinuity(userId: string, sessionData: any): void;
|
|
20
35
|
generateResponse(userInput: string, detectedState?: EmotionalState): any;
|
|
21
36
|
requiresRrtaHandoff(userState: EmotionalState): boolean;
|
|
22
37
|
}
|
package/dist/protocol.js
CHANGED
|
@@ -12,7 +12,17 @@ class SleepwalkerProtocol {
|
|
|
12
12
|
constructor(options = {}) {
|
|
13
13
|
this.loggingEnabled = options.loggingEnabled !== false;
|
|
14
14
|
const toiLoader = new toiLoader_1.TOILoader(options.userToiPath);
|
|
15
|
-
|
|
15
|
+
const loadedToi = options.userToiPath ? toiLoader.load() : {};
|
|
16
|
+
// A malformed TOI can parse to a non-object; coerce so downstream `.swp`
|
|
17
|
+
// lookups are always safe.
|
|
18
|
+
this.userToi = loadedToi && typeof loadedToi === 'object' ? loadedToi : {};
|
|
19
|
+
// Stable continuity identity for this instance. Never derive this from the
|
|
20
|
+
// user's input text — doing so makes every interaction look like a brand new
|
|
21
|
+
// user and continuity can never be retrieved. Accept an explicit id, then a
|
|
22
|
+
// top-level or swp-nested TOI id. The id is hashed where it becomes a
|
|
23
|
+
// filename (see ContinuityManager.userFile), so traversal is contained.
|
|
24
|
+
const swpToi = this.userToi.swp && typeof this.userToi.swp === 'object' ? this.userToi.swp : {};
|
|
25
|
+
this.userId = options.userId || this.userToi.user_id || swpToi.user_id || 'default_user';
|
|
16
26
|
this.stateDetector = new stateDetection_1.StateDetector();
|
|
17
27
|
this.consentManager = new consent_1.ConsentManager(this.userToi);
|
|
18
28
|
this.continuityManager = new continuity_1.ContinuityManager(options.storagePath || '.swp_storage');
|
|
@@ -26,25 +36,38 @@ class SleepwalkerProtocol {
|
|
|
26
36
|
}
|
|
27
37
|
return state;
|
|
28
38
|
}
|
|
29
|
-
assessInteraction(userInput, sessionHistory = []) {
|
|
39
|
+
assessInteraction(userInput, sessionHistory = [], userId) {
|
|
30
40
|
const emotionalState = this.detectEmotionalState(userInput, sessionHistory);
|
|
31
41
|
const consentLevel = this.consentManager.determineLevel(emotionalState);
|
|
42
|
+
// Get continuity context, keyed by the stable user identifier. Passing
|
|
43
|
+
// `userInput` here (the previous behavior on the Python side) keyed
|
|
44
|
+
// continuity on the message text, so it always reported "no history".
|
|
45
|
+
const continuityContext = this.continuityManager.getContext(userId || this.userId);
|
|
32
46
|
return {
|
|
33
47
|
emotionalState,
|
|
34
48
|
consentLevel,
|
|
49
|
+
continuityContext,
|
|
35
50
|
swpActive: this.userToi.swp?.active !== false,
|
|
36
51
|
protectiveStateActive: emotionalState.protective,
|
|
37
52
|
};
|
|
38
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Preserves emotional boundaries across sessions. Mirrors Python
|
|
56
|
+
* `maintain_continuity`: the read (`assessInteraction`) and the write
|
|
57
|
+
* (`maintainContinuity`) stay separate so that assessment never implicitly
|
|
58
|
+
* writes to disk.
|
|
59
|
+
*/
|
|
60
|
+
maintainContinuity(userId, sessionData) {
|
|
61
|
+
this.continuityManager.saveSession(userId, sessionData);
|
|
62
|
+
}
|
|
39
63
|
generateResponse(userInput, detectedState) {
|
|
40
64
|
const state = detectedState || this.detectEmotionalState(userInput);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
65
|
+
// Crisis / check-in states are handled BEFORE the protective low-demand
|
|
66
|
+
// branch. An input can be both protective (e.g. "numb" -> dissociation) and a
|
|
67
|
+
// crisis ("kill myself" -> suicidal ideation); the crisis path must win so a
|
|
68
|
+
// safety check / RRTA handoff is never masked by a stable-low-demand response.
|
|
69
|
+
// This matches ConsentManager.determineLevel, which already ranks crisis
|
|
70
|
+
// (RRTA_HANDOFF) and check-in (SAFETY_CHECK) above protective.
|
|
48
71
|
if (state.requiresCheckIn) {
|
|
49
72
|
const level = this.consentManager.determineLevel(state);
|
|
50
73
|
return {
|
|
@@ -54,6 +77,13 @@ class SleepwalkerProtocol {
|
|
|
54
77
|
intervention: 'consent_required',
|
|
55
78
|
};
|
|
56
79
|
}
|
|
80
|
+
if ((this.userToi.swp?.active !== false) && state.protective) {
|
|
81
|
+
return {
|
|
82
|
+
responseType: 'stable_low_demand',
|
|
83
|
+
guidance: 'Maintain stable, task-focused interaction',
|
|
84
|
+
intervention: 'none',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
57
87
|
return {
|
|
58
88
|
responseType: 'neutral',
|
|
59
89
|
guidance: 'Provide task-focused support',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neurolift-technologies/sleepwalker-protocol",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Emotional Continuity Governance for AI Systems",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "⚠️ PROTOTYPE / not medical advice — Emotional Continuity Governance for AI Systems",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"publishConfig": {
|
|
@@ -22,13 +22,16 @@
|
|
|
22
22
|
"mental-health",
|
|
23
23
|
"neurodivergent",
|
|
24
24
|
"ai-ethics",
|
|
25
|
-
"trauma-informed"
|
|
25
|
+
"trauma-informed",
|
|
26
|
+
"prototype",
|
|
27
|
+
"experimental",
|
|
28
|
+
"not-medical-advice"
|
|
26
29
|
],
|
|
27
30
|
"author": "NeuroLift Technologies / HAIEF <haief@neuroliftsolutions.com>",
|
|
28
31
|
"license": "Apache-2.0",
|
|
29
32
|
"repository": {
|
|
30
33
|
"type": "git",
|
|
31
|
-
"url": "https://github.com/NeuroLift-Technologies/sleepwalker.git"
|
|
34
|
+
"url": "git+https://github.com/NeuroLift-Technologies/sleepwalker.git"
|
|
32
35
|
},
|
|
33
36
|
"bugs": {
|
|
34
37
|
"url": "https://github.com/NeuroLift-Technologies/sleepwalker/issues"
|